Table driven tests with testify mock

3.3k Views Asked by At

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

There are 1 best solutions below

2
On BEST ANSWER

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. "", [email protected], 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.