douzhan1868 2017-08-06 23:03
浏览 86
已采纳

使用testify模拟的表驱动测试

Are there any examples of writing clean table driven tests using testify. A table driven test for input and expected output works well but having to test the output from a dependency seems to be really hard to do.

The below example uses one mocked interface and requires me to write a whole new test function to verify the function under test handles dependency errors properly. I am only looking for suggestions to make writing unit tests with the testify mock package more streamlined.

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条回答 默认 最新

  • doujun5009 2017-08-07 08:18
    关注

    Writing unit tests is relatively easy. Writing good unit tests is hard. This isn't helped because we are introduced to unit testing with trivial code examples that don't mimic real life usage.

    Try to avoid mocking unless you need to verify the invocations of a dependency. Prefer using stubs, fakes or real implementations. Knowing when to use each is a matter of experience and where the difficulty comes in. Also, think about your design. If you are finding it difficult to unit test, this could be because you need to redesign.

    Unit tests take time to write and maintain. You will always be quicker writing code without unit tests. However, we write unit tests to give us some assurance that our code works correctly and confidence to re-factor.

    Hence it's important to try to write the test against the behaviour (black box) instead of the implementation (white-box). This isn't always possible but unit tests that are tied to the implementation are fragile, discourage refactoring and can also sometimes mask unexpected behaviour.

    Some unit testing resources worth reading:

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

    As way as an example, think of writing a unit test for a simple email address validator. We want to write a function that will take a string and return true/false based on whether a valid email address was supplied.

    A trivial example implementation would be:

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

    We would then write a table driven test with the various inputs, e.g. "", good@example.com, bad etc and verify the result was correct.

    Now this is a bit of a trivial example but illustrates my point. One may argue that this is easy because the function has no dependencies but it does! We are relying on the regexp implementation and the regular expression we are passing it.

    This is testing the desired behaviour, not how we implement it. We don't care how it validates an email address, simply that it does. If we were to tweak the regular expression or completely change the implementation then none of this would break the tests unless the result was incorrect.

    Very few would suggest that we should isolate the dependency and test the validation function by mocking the regexp and ensuring that it is called with the regular expression we expect. This would be far more fragile but also less useful, i.e. how would we know the regular expression is actually going to work?


    For your specific example, you could easily avoid mocking & use a trivial fake to test both normal results and the error cases. This would be something like:

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

    Then simply use fake as your dependency and run your table based tests as normal.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 数据可视化Python
  • ¥15 要给毕业设计添加扫码登录的功能!!有偿
  • ¥15 kafka 分区副本增加会导致消息丢失或者不可用吗?
  • ¥15 微信公众号自制会员卡没有收款渠道啊
  • ¥15 stable diffusion
  • ¥100 Jenkins自动化部署—悬赏100元
  • ¥15 关于#python#的问题:求帮写python代码
  • ¥20 MATLAB画图图形出现上下震荡的线条
  • ¥15 关于#windows#的问题:怎么用WIN 11系统的电脑 克隆WIN NT3.51-4.0系统的硬盘
  • ¥15 perl MISA分析p3_in脚本出错