表驱动测试与 testify 模拟

nit*_*alh 4 testing mocking table-driven go testify

是否有任何使用 testify 编写干净的表驱动测试的示例。输入和预期输出的表驱动测试效果很好,但必须测试依赖项的输出似乎真的很难做到。

下面的示例使用一个模拟接口,并要求我编写一个全新的测试函数来验证被测函数是否正确处理依赖项错误。我只是在寻找建议,使使用 testify 模拟包编写单元测试更加简化。

package packageone

import (
    "errors"
    "musings/packageone/mocks"
    "testing"
)
//Regular Table driven test
func TestTstruct_DoSomething(t *testing.T) {
    testObj := new(mocks.Dinterface)

    passes := []struct {
        Input  int
        Output int
    }{{0, 0}, {1, 1}, {2, 4}, {100, 10000}}

    for _, i := range passes {
        testObj.On("DoSomethingWithD", i.Input).Return(i.Output, nil)
    }

    type fields struct {
        DC Dinterface
    }
    type args struct {
        i int
    }
    tests := []struct {
        name    string
        fields  fields
        args    args
        wantRes int
        wantErr bool
    }{
        {"Pass#0", fields{testObj}, args{passes[0].Input}, passes[0].Output, false},
        {"Pass#1", fields{testObj}, args{passes[1].Input}, passes[1].Output, false},
        {"Pass#2", fields{testObj}, args{passes[2].Input}, passes[2].Output, false},
        {"Pass#3", fields{testObj}, args{passes[3].Input}, passes[3].Output, false},
        {"Fail#4", fields{testObj}, args{-1}, 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            r := &Tstruct{
                DC: tt.fields.DC,
            }
            gotRes, err := r.DoSomething(tt.args.i)
            if (err != nil) != tt.wantErr {
                t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if gotRes != tt.wantRes {
                t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
            }
        })
    }
}

//Separate Unit test for dependency returning errors.
func TestTstruct_ErrMock_DoSomething(t *testing.T) {
    testObj := new(mocks.Dinterface)
    testObj.On("DoSomethingWithD", 1).Return(0, errors.New(""))

    type fields struct {
        DC Dinterface
    }
    type args struct {
        i int
    }
    tests := []struct {
        name    string
        fields  fields
        args    args
        wantRes int
        wantErr bool
    }{
        {"Test#1", fields{testObj}, args{1}, 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            r := &Tstruct{
                DC: tt.fields.DC,
            }
            gotRes, err := r.DoSomething(tt.args.i)
            if (err != nil) != tt.wantErr {
                t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if gotRes != tt.wantRes {
                t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
            }
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*ell 6

编写单元测试相对容易。编写好的单元测试很难。这无济于事,因为我们通过不模仿现实生活使用的琐碎代码示例介绍了单元测试。

除非您需要验证依赖项的调用,否则尽量避免模拟。更喜欢使用存根、伪造或真实的实现。知道何时使用每个是经验问题以及困难来自哪里。此外,请考虑您的设计。如果您发现很难进行单元测试,这可能是因为您需要重新设计。

单元测试需要时间来编写和维护。在没有单元测试的情况下,您将始终更快地编写代码。然而,我们编写单元测试是为了给我们一些保证我们的代码正确工作和重构的信心。

因此,尝试针对行为(黑盒)而不是实现(白盒)编写测试是很重要的。这并不总是可行的,但与实现相关的单元测试是脆弱的,不鼓励重构,有时还会掩盖意外行为。

一些值得一读的单元测试资源:

  1. 模拟不是存根
  2. 在厕所博客上进行测试
  3. TDD - 哪里出了问题

举个例子,考虑为一个简单的电子邮件地址验证器编写一个单元测试。我们想编写一个函数,该函数将接受一个字符串并根据是否提供了有效的电子邮件地址返回真/假。

一个简单的示例实现是:

var re = regexp.MustCompile("[regular expression]")
func ValidateEmail(s string) bool {
   return re.MatchString(s)
}
Run Code Online (Sandbox Code Playgroud)

然后,我们会写驱动的测试与各种输入的表格,例如""good@example.combad等,并验证结果是正确的。

现在这是一个微不足道的例子,但说明了我的观点。有人可能会争辩说这很容易,因为该函数没有依赖关系,但它确实如此!我们依赖于 regexp 实现和我们传递它的正则表达式。

这是测试所需的行为,而不是我们如何实现它。我们不关心它如何验证电子邮件地址,只是它会验证。如果我们要调整正则表达式或完全改变实现,那么除非结果不正确,否则这些都不会破坏测试。

很少有人会建议我们应该通过模拟正则表达式并确保使用我们期望的正则表达式调用它来隔离依赖项并测试验证函数。这会更脆弱但也不太有用,即我们如何知道正则表达式实际上会起作用?


对于您的具体示例,您可以轻松避免模拟并使用微不足道的假来测试正常结果和错误情况。这将是这样的:

// Used to test error result, 
var errFail = errors.New("Failed")

// Fake type
type fakeD func(input int) (int, error)

// Implements Dinterface
func (f fakeD) DoSomethingWithD(input int) (int, error) {
    return f(input)
}

// Fake implementation. Returns error on input 5, otherwise input * input
var fake fakeD = func(input int) (int, error) {
    if input == 5 {
        return nil, errFail
    }
    return input * input, nil
}
Run Code Online (Sandbox Code Playgroud)

然后简单地fake用作您的依赖项并正常运行基于表的测试。