Mocking and test data generation
For mocking I like to use mockery with testify/mock. Generally the syntax is quite clear:
mock.EXPECT().GetRunnerDetails(1).Return(nil, &gitlab.Response{}, errors.New("Something went wrong")).Once()
The most problems occur when it comes to multiple calls with different data. I was always search I to improve that without blowing up the testing code and making it unreadable. With one of my projects(clinar) I now use data generation functions like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func mockGetRunnerDetails(mock *mocks.GitLabClient, numOfCalls int) {
for i := 1; i <= numOfCalls; i++ {
details := &gitlab.RunnerDetails{
ID: int(i),
Name: fmt.Sprintf("someRunner%d", i),
Projects: []struct {
ID int "json:\"id\""
Name string "json:\"name\""
NameWithNamespace string "json:\"name_with_namespace\""
Path string "json:\"path\""
PathWithNamespace string "json:\"path_with_namespace\""
}{
{
ID: 10 + i,
Name: fmt.Sprintf("Project%d", i),
},
},
Groups: []struct {
ID int "json:\"id\""
Name string "json:\"name\""
WebURL string "json:\"web_url\""
}{
{
ID: 20 + i,
Name: fmt.Sprintf("Group%d", i),
},
},
}
mock.EXPECT().GetRunnerDetails(i).Return(details, &gitlab.Response{TotalItems: 1, TotalPages: 1}, nil).Once()
}
}
With that I can easily setup multiple calls with different Return values. Which can also fail if necessary. It needs still some code to generate the data, but it doesn’t blow up the test code. The basic testing code looks quite clear and is focused on the expectations of the tests. Here is an example how this can look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
t.Run("Simple case", func(t *testing.T) {
mock := &mocks.GitLabClient{}
logger, logHook := logrusTest.NewNullLogger()
mockDeleteRegisteredRunnerByID(mock, 5)
clinar := Clinar{Client: mock, Logger: logger}
clinar.CleanupRunners([]*gitlab.RunnerDetails{{ID: 1}, {ID: 2}, {ID: 3}, {ID: 4}, {ID: 5}})
mock.AssertExpectations(t)
assert.Len(t, logHook.Entries, 5)
assert.Contains(t, logHook.Entries[0].Message, "Deleting")
assert.Contains(t, logHook.Entries[1].Message, "Deleting")
assert.Contains(t, logHook.Entries[2].Message, "Deleting")
assert.Contains(t, logHook.Entries[3].Message, "Deleting")
assert.Contains(t, logHook.Entries[4].Message, "Deleting")
// Wait 50ms until goroutines are finished
})
Using data generation functions works well with testify/mock and the code looks quite clean. The only thing which must be considered is that the mock must be defined within the test case (as shown above). Otherwise you could get unexpected behavior as the old expectation will be still present and match on your arguments. Also the AssertExpectations wil then fail.
One note on logrus in unittests. My recommendation is to use a New If you want to assert on log entries using the logrus testing hook, you must use a new log instance. As this avoids getting strange results from the standard instance. Especially when working with multiple go routines.