More Mocking Techniques
Mocking Power - Interfaces, Testify, and HTTP for Go Testing.
More Mocking Techniques
GoMock is convenient and feature-rich, but there might be occasions where other mocking techniques are a better fit, or you may simply prefer not to use code generators or framework-like package APIs.
So, let's look into some alternatives to GoMock.
Manual Mock Testing with Interfaces
This is the "purest" approach to mock testing. No code generators, no third-party packages—only your source code and you.
For your simple use case, creating a mock fetcher manually is done in a few steps.
First, you create a struct that holds the test data and implements FetchData()
. In FetchData()
, you can implement the desired mocking behavior. The demo code assumes a single test that fetches the user with an ID of 1 (see fetchuser_interface_test.go
):
type MockInterfaceFetcher struct {
u User
}
func (m *MockInterfaceFetcher) FetchData(_ int) (User, error) {
return m.u, nil
}
Then, in the test function, you can set up test data, create a struct with this test data, and pass this struct to ProcessUser()
:
func TestProcessUser_InterfaceMock(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
result, err := ProcessUser(&MockInterfaceFetcher{user}, 1)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != user {
t.Errorf("Expected user: %v, got: %v", user, result)
}
}
Manual mocking is the most lightweight approach, with no external dependencies or command line utilities.
Using testify/mock
The testify/mock
package (part of the stretchr/testify toolkit) falls somewhere in the middle between GoMock and manual mocking. It provides helper methods but does not use code generation.
testify/mock
works by embedding a mock.Mock
type into the mock struct:
type MockTestifyFetcher struct {
mock.Mock
}
The mock method FetchData()
then only calls the mock.Mock.Called(args...)
method and returns the appropriate values from the returned mock.Arguments
type.
Note that mock.Mock
is an anonymous, embedded field, and hence the FetchData()
method can call m.Called()
directly instead of m.Mock.Called()
:
func (m *MockTestifyFetcher) FetchData(id int) (User, error) {
args := m.Called(id)
return args.Get(0).(User), args.Error(1)
}
In the test function, you create test data and a new MockTestifyFetcher
variable.
You then call Mock.On()
(which, like Mock.Called()
, is elevated and can be called directly as a method of MockTestifyFetcher
) and chain it to a call to Return()
.
In the code below, On()
takes the name of the method to call and the arguments for that method, and Return()
specifies the return arguments of the mock method:
func TestProcessUser_TestifyMock(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
mockFetcher := new(MockTestifyFetcher)
mockFetcher.On("FetchData", 1).Return(user, nil)
result, err := ProcessUser(mockFetcher, 1)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != user {
t.Errorf("expected user: %v, got: %v", user, result)
}
}
As a result, when the test calls ProcessUser(mockFetcher)
, the invoked method FetchData()
calls m.Called()
to receive a slice of return values. It returns the User
and error
elements from that slice so that the test function can inspect the return values.
Making Mock HTTP Requests with httptest
This mocking technique is the only one that does not rely on an interface as an input parameter to the tested function. This is because the situation is rather specific: the httptest
package redirects HTTP requests to a local test server. Therefore, there is no need to change the FetchData()
function. You can use the original RealAPIFetcher
type with a different ApiURL
value.
Here's how it works.
First, inside the test function, set up a new httptest.Server
with a HandlerFunc()
that responds with test data.
Make sure you add a deferred call to its Close()
method:
func TestProcessUser_HttpTest(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userJSON, _ := json.Marshal(user)
n, err := w.Write(userJSON)
if err != nil {
t.Errorf("test server: unexpected error after writing %d bytes: %v", n, err)
}
}))
defer ts.Close()
Next, create a RealAPIFetcher
object. There is no need for a mock object here. You only need to set ApiURL
to a URL that the httptest.Server
object conveniently provides.
You then replace http.DefaultClient
with a custom client provided by httptest.Server
. This client is configured to make requests to the test server. It trusts the server's TLS certificate and closes its idle connections when the deferred Close()
method of the server is called:
fetcher := &RealAPIFetcher{
ApiURL: ts.URL,
}
http.DefaultClient = ts.Client()
The rest is standard testing. Call ProcessUser()
with the RealAPIFetcher
as an argument and verify the result:
result, err := ProcessUser(fetcher, 1)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != user {
t.Errorf("expected user: %v, got: %v", user, result)
}
}
httptest
is the mock technique that is most like calling the real server. It uses the http
package, does not reimplement the APIFetcher
interface, and performs real HTTP calls.
Mocking with Higher-Order Functions
Functions in Go are first-class types. They can be assigned to variables, passed to other functions as arguments, and even have their own methods. You can use this feature to implement mocking through higher-order functions. (See fetchuser_higherorderfunctions.go
in the repository.)
Higher-order functions are functions that take one or more functions as arguments, return a function, or do both.
First, you need to modify ProcessUser()
to take a function instead of an interface.
To keep the parameter list short and readable, it's good practice to first define a function type:
type FetchDataFunc func(url string, id int) (User, error)
Note that this function replaces the APIFetcher
interface and the structs that implement the interface. Because the structs hold the API URL, the FetchData()
method only needs an id
parameter. FetchDataFunc()
does not have a place to store the URL, so it receives the URL directly.
Next, you can rewrite ProcessUser()
to ProcessUserHOF()
, which takes a function of type FetchDataFunc
instead of the interface:
func ProcessUserHOF(fetchData FetchDataFunc, url string, id int) (User, error) {
user, err := fetchData(url, id)
if err != nil {
return User{}, err
}
// Process the user data.
return user, nil
}
You can now provide different FetchDataFunc
functions to ProcessUserHOF()
.
First, add a function for production, which is basically the same as the FetchData()
method but with an additional url
parameter:
func RealFetchData(url string, id int) (User, error) {
resp, err := http.Get(fmt.Sprintf("%s/users/%d", url, id))
if err != nil {
return User{}, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return User{}, err
}
var user User
err = json.Unmarshal(bodyBytes, &user)
return user, err
}
Second, add a mock function inside your tests:
func TestProcessUser_HigherOrderFunctions(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
var mockFetcher FetchDataFunc = func(url string, id int) (User, error) {
return user, nil
}
result, err := ProcessUserHOF(mockFetcher, "noURL", 1)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != user {
t.Errorf("Expected user: %v, got: %v", user, result)
}
}
This technique is especially useful if the code to be replaced is a single function.