GoMock allows you to simulate dependencies and removes reliance on other teams or systems. In other words, GoMock can generate a response that matches your dependency without using the dependency.
Imagine you’re developing a payment processing application that relies on a third-party payment processing API. You’ve written several tests—unit tests, integration tests, end-to-end tests, etc.—yet you find that the unreliability of the payment processing API is often the cause of test failures.
By mocking the API, i.e. by simulating its responses, you can validate all the functionality of your application without the performance of the real API playing a role. This post covers how to implement GoMock in your application alongside the benefits and limitations of mocks.
What is GoMock and Why Choose It?
GoMock is a testing framework provided by the Golang team. It integrates with the rest of the Go ecosystem, like go:generate and the built-in testing
package. GoMock isn’t the only Golang testing framework, but it’s the preferred tool for many because of its powerful type-safe assertions.
As you’ll see in the next section, GoMock works by generating a Mock Object from interfaces, meaning GoMock requires interfaces to function. In practical terms, generating a Mock Object is the creation of a new file that contains an implementation of your interface and all the logic needed to create stubs and define mock behavior.
Choosing GoMock—or any mocking tool—depends on solving dependency issues in testing. While integration tests are valuable, as they test the entire chain of a request, they’re often time-consuming, complex, and/or costly. Additionally, if your mocks are properly configured and match the real dependency, you may find a decreased need to perform full integration tests in the first place.
Essentially, mocks provide unit testing ease and most of the benefits from integration testing. You get to test your code changes in isolation while validating interoperability with other applications.
Prerequisites
Before you dive into the tutorial part of this post, make sure you meet a few prerequisites.
- A basic understanding of the Go programming language
While not strictly necessary, this tutorial is more useful if you have a good grasp of Go’s structure. If not, start learning at GoByExample.
- A system with Go installed
This tutorial is written using Go v1.20.1. Ensure you’ve installed this version or a later one.
Installing GoMock
Before working with GoMock, you need to install it. Do this by running:
$ go install github.com/golang/mock/mockgen@v1.6.0
Note that this installs the tool mockgen
into $GOPATH/bin
. Mockgen.
The tool is used to generate Mock Objects. But it isn’t necessary to make GoMock work because you can write Mock Objects without the tool. However, the tool makes the process much easier.
Next, install GoMock in your Go project. First you have to initialize your project as a Go Module:
$ mkdir $GOPATH/src/gomock-getting-started && \ cd $GOPATH/src/gomock-getting-started && \ go mod init gomock-getting-started
Start adding any necessary modules to your project, like GoMock:
$ go get github.com/golang/mock/gomock
By now, you’ve installed everything you need, and you can start creating Mock Objects to test your application. To do so in this tutorial, you need to create an application first.
Creating Your First Mock with GoMock
Keeping with the example from the introduction, let’s make a simple program that resembles an API interacting with a payment processor. To do so, create a main.go
file that implements an interface and two structs.
NOTE: All code can be found in this GitHub repo.
$ cat <<EOF > main.go package main import ( "net/http" "bytes" "encoding/json" "errors" ) // Interface defining functions for PaymentProcessor implementation type PaymentProcessor interface { Charge(amount float64, token string) error } // Defines the struct for StripePaymentProcessor, which will implement the PaymentProcessor interface type StripePaymentProcessor struct{} // Implementation of the Charge function defined in the PaymentProcessor interface func (s *StripePaymentProcessor) Charge(amount float64, token string) error { // Define empty json object for use in HTTP request data := map[string]string{} json_data, err := json.Marshal(data) // execute HTTP request resp, err := http.Post("https://api.stripe.com/v1/charges", "", bytes.NewBuffer(json_data)) // Return error if any exists if err != nil { return err } // Decode the response to json // Note: this isn't used anywhere, as this function definition only returns an error // This is done purely to simplify the example var res map[string]interface{} json.NewDecoder(resp.Body).Decode(&res) return nil } // Define the PaymentProcessorClient that is going to be tested type PaymentProcessorClient struct { PaymentProcessor PaymentProcessor } // Define the Charge function for the client, which is going to be tested func (c *PaymentProcessorClient) Charge(amount float64, token string) error { // Define the lowest possible charge if amount < 20 { return errors.New("Charge too low") } return c.PaymentProcessor.Charge(amount, token) } EOF
Understanding the specifics of every single code line isn’t necessary, although you can read the inline comments for more info. But you need to understand that there’s a PaymentProcessor
interface that defines a Charge
function. Then, the StripePaymentProcessor
implements this interface and interacts with the Stripe API. The function body mainly showcases the typical flow of such requests. But you don’t need it in this tutorial because that isn’t what you’ll be mocking.
After this, create a PaymentProcessorClient
struct that uses the PaymentProcessor
interface, as shown in the Charge
function, where it’s passing the function parameters to the interface implementation.
The principle behind this code is that the PaymentProcessorClient
can interact with any payment processor. In this case, it’s Stripe, but you can easily swap it because it’s using an interface.
Now that you’ve defined a simple application with an interface, you can generate your first Mock Object.
Creating a Mock Object
After defining your interfaces, use mockgen
to generate your Mock Object by defining the file containing your interfaces and the destination of the Mock Object you generate.
$ mockgen -source main.go -destination mocks/mocks.go
If you look at the newly-generated mocks/mocks.go
file, you’ll see that it’s created a number of structs
. You don’t need to understand all the code, but here’s what structs
are created:
$// Code generated by MockGen. DO NOT EDIT. // Source: main.go // Package mock_main is a generated GoMock package. package mock_main import ( reflect "reflect" gomock "github.com/golang/mock/gomock" ) // MockPaymentProcessor is a mock of PaymentProcessor interface. type MockPaymentProcessor struct { ctrl *gomock.Controller recorder *MockPaymentProcessorMockRecorder } // MockPaymentProcessorMockRecorder is the mock recorder for MockPaymentProcessor. type MockPaymentProcessorMockRecorder struct { mock *MockPaymentProcessor } // NewMockPaymentProcessor creates a new mock instance. func NewMockPaymentProcessor(ctrl *gomock.Controller) *MockPaymentProcessor { mock := &MockPaymentProcessor{ctrl: ctrl} mock.recorder = &MockPaymentProcessorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockPaymentProcessor) EXPECT() *MockPaymentProcessorMockRecorder { return m.recorder } // Charge mocks base method. func (m *MockPaymentProcessor) Charge(amount float64, token string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Charge", amount, token) ret0, _ := ret[0].(error) return ret0 } // Charge indicates an expected call of Charge. func (mr *MockPaymentProcessorMockRecorder) Charge(amount, token interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Charge", reflect.TypeOf((*MockPaymentProcessor)(nil).Charge), amount, token) }
MockPaymentProcessor
is the main struct
that makes GoMock work because it contains the controller and recorder for your tests. This is GoMock’s brain, as you’ll see in the implementation shortly. To view the description of each part of the Mock Object, use the inline comments.
Configuring GoMock
By now, you’re ready to start using mocks inside your tests. Start by creating a main_test.go
in your root directory with all the boilerplate code needed:
$ cat <<EOF > main_test.go package main_testimport ( "testing" "gomock-getting-started" "gomock-getting-started/mocks" "github.com/golang/mock/gomock" "errors" ) func TestCharge(t *testing.T) { } EOF
Note that it’s important to save your project to $GOPATH/src
because Go uses the path for local imports. It’s why "gomock-getting-started"
and "gomock-getting-started/mocks"
works and placing your code anywhere else will result in an error.
With the boilerplate code in place, start building the test. Start the test by defining the mock controller and use it to generate the mock PaymentProcessor and the test PaymentProcessorClient, which you’ll use to test the Charge()
function:
func TestCharge(t *testing.T) {
mockCtrl := gomock.NewController(t)
mockPaymentProcessor := mock_main.NewMockPaymentProcessor(mockCtrl)
testPaymentProcessorClient := &main.PaymentProcessorClient{ PaymentProcessor: mockPaymentProcessor }
}
The next step is ensuring that GoMock verifies any expectations you set during mock behavior configuration, like the number of times a given function needs to be called:
...
testPaymentProcessorClient := &main.PaymentProcessorClient{ PaymentProcessor: mockPaymentProcessor }
defer mockCtrl.Finish()
}
Define the behavior of the Mock Object before writing the tests for the PaymentProcessorClient’s Charge()
function. Although GoMock and mockgen
are smart, they can’t analyze your code to generate responses. This is why you should analyze your code as part of your test. Do this using the EXPECT()
function:
...
defer mockCtrl.Finish()
mockPaymentProcessor.EXPECT().Charge(100.0, "test_token").Return(nil).Times(1)
}
Here, the EXPECT()
call is immediately followed by the.Charge()
call where you define the parameters the mock implementation of the PaymentProcessor’s Charge()
function should accept. Your test will fail if any of the test code makes calls to the Charge()
function not matching the parameters you’ve defined.
To avoid hard coding you can replace parameters with gomock.Any()
. However, where possible, use specific parameters to make your tests less ambiguous.
The last two functions Return(nil)
and Times(1)
define that a call to Charge(100.0, "test_token")
should return nil.
But the call has to be used once during the Mock Object lifetime. Use AnyTimes()
to avoid specifics about the number of times you call a function, although this should be avoided when possible to reduce ambiguity. Now that the Mock Object is fully configured, start writing your test.
Writing Tests With the Mock Object
To call the function in your test is like calling it anywhere else in your codebase:
...
mockPaymentProcessor.EXPECT().Charge(100.0, "test_token").Return(nil).Times(1)
err := testPaymentProcessorClient.Charge(100.0, "test_token")
}
With the function having been called, validate the return value of the function:
...
err := testPaymentProcessorClient.Charge(100.0, "test_token")
if err != nil {
t.Fail()
}
}
This is a simple test case like other tests you’d write in Go. If there’s an error, the test fails. If not, it passes. If you look at the main.go
file, you’ll see that the PaymentProcessor.Charge()
function also contained logic that validates the charge amount not being below 20. Let’s write a test for that.
...
if err != nil {
t.Fail()
}
err = testPaymentProcessorClient.Charge(10.0, "test_token")
if err.Error() != "Charge too low"; {
t.Errorf("Error returned was: %s", err.Error())
}
}
Here, you see the same err
variable being used as before, but the Charge
amount has been changed to 10.0
. Then, the code is verifying that the error response matches what’s been defined in the function implementation in main.go
. Now, verify that both tests are working as expected by running:
$ go test
PASS
ok gomock-getting-started 0.003s
The Mock works as expected, and you’ve successfully verified the implementation of Charge()
without using the actual Stripe API. A clear indicator is getting no errors, athough the call to Stripe defined in main.go
is not a proper implementation.
You can also verify this by modifying the mocks to purposefully fail:
...
// mockPaymentProcessor.EXPECT().Charge(10.0, "test_token").Return(errors.New("Error in Stripe")).AnyTimes()
mockPaymentProcessor.EXPECT().Charge(30.0, "test_token").Return(errors.New("Error in Stripe")).AnyTimes()
...
// err = testPaymentProcessorClient.Charge(10.0, "test_token")
err = testPaymentProcessorClient.Charge(30.0, "test_token")
...
This time the test should fail:
$ go test
--- FAIL: TestCharge (0.00s)
main_test.go:44: Error returned was: Error in Stripe
FAIL
exit status 1
FAIL gomock-getting-started 0.003s
This is a simple example of how to use GoMock in your tests; however, it’s far from a comprehensive overview. The official GoMock README is a great place to get started if you want to learn more.
Limitations of GoMock
As seen in this post, using GoMock isn’t too complex. But consider some of the limitations before integrating it into your infrastructure.
Only works with Go
You’re unlikely to have issues if your organization only uses Go. But many organizations are switching to microservices architecture that use different programming languages. In this case—or cases of uncertainties about the future use of Go—consider using a language-agnostic tool instead.
Sits directly in your codebase
A GoMock living as code directly in your codebase is positive and negative. It allows easy structure and access to writing tests.
But the inability to create a central system for tests is a possible drawback. For example, deciding whether to use a new version of GoMock organization-wide is time-consumning, depending on the number of individual projects.
Requires significant setup
As seen in this post, setting GoMock up for a single project isn’t too complicated, although it’s time-consuming and complex when you have many applications. You can add projects manually or automate the process. But neither option is easy or quick to execute.
Mostly suited for unit testing
Although it’s the core idea behind GoMock, you must know it’s only suited for unit testing, not integration and end-to-end testing. It means implementing other types of testing inherently involves adding other tools.
Only works with interfaces
If you’re generating Mock Objects with mockgen,
then you’re limited to mocking interfaces. The upside is that it encourages the use of interfaces, which is arguably a good development paradigm. But it’s a limitation you should consider.
—
Overall, GoMock is a great tool for unit testing in Go. However, consider using user traffic to build a mock server, which addresses all the limitations above. Irrespective of the type of testing you’re doing, you’ll gain troves of advantages by using mocks.