4

是否有任何使用 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)
            }
        })
    }
}
4

1 回答 1

6

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

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

单元测试需要时间来编写和维护。在没有单元测试的情况下,您将始终更快地编写代码。但是,我们编写单元测试是为了让我们确信我们的代码可以正常工作并有信心重构。

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

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

  1. 模拟不是存根
  2. 在厕所博客上测试
  3. TDD - 一切都出错了

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

一个简单的示例实现是:

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

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

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

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

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


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

// 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其用作您的依赖项并正常运行基于表的测试。

于 2017-08-07T08:18:12.737 回答