-
Notifications
You must be signed in to change notification settings - Fork 0
/
unit_test.go
156 lines (125 loc) · 4.53 KB
/
unit_test.go
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package mocking
import (
"encoding/base64"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// First test implementation - not using any mock
func TestUnit_SomeFunc_1(t *testing.T) {
// arrange
// ut - is short for unit under test
// for the sake of brevity, the unit is already setup for dependency injection.
ut := unit{
dep: NewExternalDependency(),
encoder: base64.StdEncoding,
}
// act
result, err := ut.SomeFunc("bla")
// assert
is := assert.New(t)
is.Nil(err)
// 'bW9vOiBibGE=' value, produced by the base64 encoding, of 'moo: bla' which is the result of the ExternalDependency
// The test is thus coupled to the underlying implementation of the dependencies, hindering reusability of the unit.
is.Equal("bW9vOiBibGE=", result)
}
// Second test, introducing a roll our own mock to replace ExternalDependency implementation
func TestUnit_SomeFunc_2(t *testing.T) {
// arrange
dep := &mockExternalDep1{}
ut := unit{
dep: dep,
encoder: base64.StdEncoding,
}
// act
result, err := ut.SomeFunc("foo")
// assert
is := assert.New(t)
is.Nil(err)
// Again, we're testing against the expected result of the base64 encoding. In this case, the value depends on
// what has been hard coded in the mock.
is.Equal("YmFy", result)
}
// Third test, using our roll our own mocked ExternalDependency - to test an error case generated by invalid input.
// Testing the error requires modifying our mock to simulate an invalid input and return an error. This aims to
// illustrate that the provided approach requires hard coding in our mock implementation all the required input/output
// for all of our test cases, increasing rigidity (resistance to changes) of our test suite.
func TestUnit_SomeFunc_3(t *testing.T) {
// arrange
dep := &mockExternalDep1{}
ut := unit{
dep: dep,
encoder: base64.StdEncoding,
}
// act
result, err := ut.SomeFunc("invalid")
// assert
is := assert.New(t)
is.NotNil(err)
is.Equal("invalid", result)
}
// Fourth test. Replacing our mock object by one implemented with github.com/stretchr/testify/mock and also using
// a mock for the external encoder. Note that we created an interface to abstract the Base64.Encoding object with
// only the method that we are using from it. Since interfaces in golang are implicit (if an object exposes
// the same method signatures as the interface's, then it can be used as this interface), then dependencies from
// external package can be abstracted even if no interface is provided for them. Also, it allows you to write mock
// implementations only for the method you are using!
//
// Observe benefits of configuring the test cases expectation in the test + observability
func TestUnit_SomeFunc_4(t *testing.T) {
// arrange
dep := &mockExternalDep2{}
dep.
On("DoSomething", "bla").
Return("abcd", nil).
Once()
encoder := mockEncoder{}
encoder.
On("EncodeToString", []byte("abcd")).
Return("babayaga").
Once()
ut := unit{
dep: dep,
encoder: encoder,
}
// act
result, err := ut.SomeFunc("bla")
// assert
is := assert.New(t)
// this validates that our mock objects have been called with the expected values and methods, providing observability
// that was lacking in our roll our own model.
dep.AssertExpectations(t)
encoder.AssertExpectations(t)
is.Nil(err)
// yay! we are not coupled to dependencies implementation to validate our logic.
// you may point out that we are not validating the correct behaviour of our dependencies anymore, and you would
// be right! it depends what types of test you are writing, but in the context of unit tests, testing only the
// unit in scope allows for simpler and more maintainable tests, faster feedback loops and also more reusable
// units!
is.Equal("babayaga", result)
}
// Fifth test - the error case revisited. No change required to the mock implementation to support this new use cases;
// input and output expectations are configured directly in the test case, with no conflict with the other test case
// (note that the same input value of 'bla' is used for both).
func TestUnit_SomeFunc_5(t *testing.T) {
// arrange
dep := &mockExternalDep2{}
dep.
On("DoSomething", "bla").
Return("", fmt.Errorf("i am an error")).
Once()
encoder := &mockEncoder{}
ut := unit{
dep: dep,
encoder: encoder,
}
// act
result, err := ut.SomeFunc("bla")
// assert
is := assert.New(t)
dep.AssertExpectations(t)
// validating explicitly that the method has not been called, because of the previous error
encoder.AssertNotCalled(t, "EncodeToString")
is.NotNil(err)
is.Equal("", result)
}