【问题标题】:Mock random generator in GoGo 中的模拟随机生成器
【发布时间】:2019-08-02 04:00:55
【问题描述】:

出于测试目的,我想在 Go 中模拟随机数。所以我创建了 Random 界面。在单元测试期间,我返回标识函数,而为了实现,我使用 rand 包生成一个随机数。

这是在 Go 中模拟随机数的正确方法吗?任何帮助表示赞赏。

去游乐场:https://play.golang.org/p/bibNnmY2t1g

主要:

package main

import (
    "time"
    "math/rand"
    "fmt"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

type Random interface {
    Uint(_ uint) uint
}

type rndGenerator func(n uint) uint

type Randomizer struct {
    fn rndGenerator
}

func NewRandomizer(fn rndGenerator) *Randomizer {
    return &Randomizer{fn: fn}
}

func (r *Randomizer) Uint(n uint) uint {
    return r.fn(n)
}

func fakeRand(n uint) uint { return n }
func realRand(_ uint) uint { return uint(rand.Uint64()) }

func main() {
    fakeRnd := NewRandomizer(fakeRand).Uint
    fmt.Println(fakeRnd(1))
    fmt.Println(fakeRnd(2))

    realRnd := NewRandomizer(realRand).Uint
    fmt.Println(realRnd(0))
    fmt.Println(realRnd(0))
}

测试:

package main

import (
    "testing"
    "math/rand"
    "reflect"
)

func TestNewRandomizer(t *testing.T) {
    fn := func(n uint) uint { return n }

    type args struct {
        fn rndGenerator
    }
    tests := []struct {
        name string
        args args
        want *Randomizer
    }{
        {
            "test",
            args{fn},
            &Randomizer{fn},
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := NewRandomizer(tt.args.fn); reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
                t.Errorf("NewRandomizer() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestRandomer_Uint(t *testing.T) {
    rnd := uint(rand.Uint64())

    type fields struct {
        fn rndGenerator
    }
    type args struct {
        n uint
    }
    tests := []struct {
        name   string
        fields fields
        args   args
        want   uint
    }{
        {
            "test",
            fields{func(n uint) uint { return n }},
            args{rnd},
            rnd,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            r := &Randomizer{
                fn: tt.fields.fn,
            }
            if got := r.Uint(tt.args.n); got != tt.want {
                t.Errorf("Randomizer.Uint() = %v, want %v", got, tt.want)
            }
        })
    }
}

func Test_fakeRand(t *testing.T) {
    rnd := uint(rand.Uint64())

    type args struct {
        n uint
    }
    tests := []struct {
        name string
        args args
        want uint
    }{
        {
            "test",
            args{rnd},
            rnd,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := fakeRand(tt.args.n); got != tt.want {
                t.Errorf("fakeRand() = %v, want %v", got, tt.want)
            }
        })
    }
}

func Test_realRand(t *testing.T) {
    type args struct {
        in0 uint
    }
    tests := []struct {
        name string
        args args
        want bool
    }{
        {
            "test",
            args{0},
            true,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := realRand(tt.args.in0); got < 1 {
                t.Errorf("realRand() = %v, want %v", got, tt.want)
            }
        })
    }
}

【问题讨论】:

  • 模拟随机数的目的是什么? (更改底层生成器?使用第三方包?还是获取特定的测试序列?)
  • 这是为了测试目的。我所有的结构在实现中都有一个随机 id。
  • 使用 PRNG 的原因之一是输出可重复。如果每次输出都相同,那么 mock 的目标是什么?
  • 不一样,因为我使用了一个恒等函数,我将我想要的输出作为参数传递
  • 查看 Go Playground 以获取输出示例:play.golang.org/p/bibNnmY2t1g

标签: unit-testing go random


【解决方案1】:

您的示例没有真正使用Random 接口,因为模拟是在Randomizer 类型的函数字段级别完成的。

如果可能,我建议放弃函数字段以及函数,而是定义Random 接口的两个单独实现。您可以为此使用空结构,它们一开始可能看起来很奇怪,但它们具有占用 0 字节内存的好特性。

推荐的主要原因是,当您使用函数字段时,您将无法将您的结构类型值与reflect.DeepEqual 进行比较。这是因为两个函数值只有在它们具有相同类型并且都是 nil 时才相等。

作为示例,让我们首先查看您的TestNewRandomizer,这是上述问题的症状。在测试中,您只是在比较编译器已经确保的返回值的类型,因此测试绝对没有意义。

现在,假设您放弃了测试,因为它没有用,但由于某种原因您保留了函数字段。因此,任何依赖于*Randomizer 的结构类型也将无法使用DeepEqual 进行测试,并且在尝试对该类型进行测试时也会遇到同样的困难。

package main

import (
    "time"
    "math/rand"
    "fmt"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

type Random interface {
    Uint(_ uint) uint
}

type Randomizer struct {}

func NewRandomizer() Randomizer {
    return Randomizer{}
}

func (r Randomizer) Uint(n uint) uint {
    return uint(rand.Uint64())
}

type FakeRandomizer struct {}

func NewFakeRandomizer() FakeRandomizer {
    return FakeRandomizer{}
}

func (r FakeRandomizer) Uint(n uint) uint {
    return n
}

func main() {
    fakeRnd := NewFakeRandomizer().Uint
    fmt.Println(fakeRnd(1))
    fmt.Println(fakeRnd(2))

    realRnd := NewRandomizer().Uint
    fmt.Println(realRnd(0))
    fmt.Println(realRnd(0))
}

请注意,我有意返回值而不是指针,因为空结构小于指针。

【讨论】:

  • 非常感谢。我借用了您的代码并更改了 Random 方法签名以摆脱函数参数:play.golang.org/p/w2iKC1Dz0Ek
  • @SofianeCherchalli 最好让您的StudentManager 依赖于接口Random 而不是具体类型Randomizer,这样当您为NewStudent 编写测试时,您可以模拟rnd . play.golang.org/p/vibEizUH7Bg
  • 抱歉,这是半夜的拼写错误。我的意思是随机的:)
【解决方案2】:

我有一个生成随机整数的方法,如果整数小于或等于 50,则返回 true;如果整数在 [0, 100) 范围内大于 50,则返回 false

这就是我创建结构来模拟功能的方式:

type Decider struct {
    RandImpl func(int) int
}

func (d *Decider) Decide(randRange int) bool {
    randVal := d.RandImpl(randRange)
    log.Info("RandVal: ", randVal)
    if randVal <= 50 {
        return true
    }
    return false
}

我是这样调用这个方法的:

rand.Seed(time.Now().UnixNano())
decider := decide.Decider{
    RandImpl: func(x int) int { return rand.Intn(x) },
}
decider.Decide(100)

在我的_test.go 文件中,我有这个:

decider := Decider{
    RandImpl: func(x int) int { return 42 },
}
ret := decider.Decide(100)
assert.True(t, ret)

这样您就可以模拟随机数生成器的功能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-05
    • 1970-01-01
    • 2020-07-25
    • 2017-05-28
    • 2014-02-05
    • 2013-02-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多