Table 使用 testify 模拟驱动测试

Table driven tests with testify mock

是否有使用 testify 编写干净的 table 驱动测试的示例。 table 对输入和预期输出的驱动测试运行良好,但必须测试依赖项的输出似乎真的很难做到。

下面的例子使用了一个模拟接口,并要求我编写一个全新的测试函数来验证被测函数是否正确处理了依赖错误。 我只是在寻找建议,以便更简化地使用 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)
            }
        })
    }
}

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

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

编写和维护单元测试需要时间。没有单元测试,你总是会更快地编写代码。但是,我们编写单元测试是为了确保我们的代码可以正常工作,并有信心进行重构。

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

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

  1. Mocks Aren't Stubs
  2. Testing on the Toilet Blog
  3. TDD - Where it all went wrong

举个例子,考虑为一个简单的电子邮件地址验证器编写一个单元测试。我们想编写一个函数,该函数将根据是否提供了有效的电子邮件地址来获取字符串和 return true/false。

一个简单的示例实现是:

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

然后我们将使用各种输入编写一个 table 驱动测试,例如""good@example.combad 等并验证结果是否正确。

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

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

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


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

// 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
}

然后简单地使用 fake 作为你的依赖,运行 你的 table 作为正常的测试。