In this blog post I’d like to show you how to use Go’s table-driven tests with mocks to write powerful and flexible tests to simulate the behavior of depdendent components or modules of your structs.
Table-driven tests are a common testing pattern in Go. They are a way to organize and structure tests in a way that is easy to read, maintain, and extend. In Go, table-driven tests are typically implemented using a struct (i.e. a composite data type) that defines the different scenarios or test cases that need to be tested. Each test case is defined as a field in the struct, and the test case’s expected behavior is specified using one or more function arguments. This makes it easy to see at a glance what the test is testing and what the expected outcome is. Additionally, table-driven tests can be run in parallel, which can help to speed up the testing process.
Table-driven tests are a powerful and flexible way to write tests in Go, and they are widely used in the Go community.
Mocking is a technique used in software testing to simulate the behavior of dependent components or modules. This can be useful for several reasons. First, it allows tests to be run independently of the actual implementation of the dependent components, which makes the tests more portable and easier to run. Second, it allows tests to be run in isolation, which can help to prevent tests from interfering with each other and producing unreliable results. Third, mocking can be used to test how a system behaves in different scenarios, such as when a dependent component is unavailable or returns an error. This can help to identify and diagnose problems with the system. Overall, mocking is an important tool in software testing that can help to improve the reliability and robustness of a system.
Let’s look at the following project structure:
A vehicle has to components
engine. Both dependencies must be mocked in order to test the
This code defines a Go package named
vehicle that contains the implementation of an
ElectricVehicle type. The
ElectricVehicle type has two fields:
engine field is of type
engine.Engine, and the
navigation field is of type
navigation.Navigation. These types are defined in the
navigation packages, respectively.
The package also defines two global variables:
errNoNetworkConnectivity, which are errors that can be returned by the
ElectricVehicle type’s methods.
The package defines a
NewElectricVehicle function that can be used to create a new instance of the
ElectricVehicle type. The function takes two arguments: an
engine.Engine and a
navigation.Navigation, and it returns a pointer to the newly created
ElectricVehicle type has a method named
VehicleDiagnostics that takes two arguments: a
chargingThreshold and a
isOffline flag. The method returns an error if either the vehicle’s engine has a low charge level or the vehicle’s navigation is offline, and it returns
In order to test our Vehicle’s
VehicleDiagnostics function, we’ll use
testify to mock our subcomponents.
testify is a Go package that provides a set of tools for writing and running tests. It is a fork of the
stretch package, which was developed by the same author.
testify includes a number of useful features, such as the ability to assert the expected behavior of a function, the ability to mock (i.e. simulate) the behavior of dependent components, and the ability to generate test coverage reports. It is widely used in the Go community, and it is considered to be a popular and robust testing tool.
How do we mock the engine and navigation components of our Vehicle? Let’s take a look at the engine’s mock implementation first. The navigation component mocks works in a similar style
This code defines a Go package named
engine that contains the implementation of a
MockedEngine type, which is a mock implementation for our
Engine interface. The
MockedEngine type is a struct that embeds the
mock.Mock type from the
testify/mock package. This allows the
MockedEngine type to inherit the behavior and functionality of the
MockedEngine type defines a method named
ChargeLevel that returns an integer. The implementation of the
ChargeLevel method uses the
Called method from the embedded
mock.Mock type to retrieve the arguments that were passed to the method. It then uses the
Get method to retrieve the first argument and convert it to an integer, which it returns. This allows the
ChargeLevel method to be mocked (i.e. simulated) in tests, so that the behavior of the
MockedEngine type can be controlled and tested.
Our navigation component is mocked in a similar way as you can see here:
Now let’s write some table-driven tests to verify the correctness of our
Vehicle’s diagnostic function. Well start defining a simple test function in a file
vehicle_test.go in the
We’ll start by introducing new structs which we’ll need for our table tests.
The code defines two new types:
mockedFields type is a struct (i.e. a composite data type) that has two fields:
navigation field is a pointer to a
MockedNavigation type, and the
engine field is a pointer to a
MockedEngine type. These types are defined in the
engine packages, respectively.
args type is also a struct, and it has two fields:
lowBatteryThreshold field is an integer, and the
isOffline field is a boolean. These fields correspond to the arguments that are passed to the
VehicleDiagnostics function. These two types are used later in the code to define the test cases for the
Let’s continue by defining some test cases
Let’s not go too much into details what values we’re passing as arguments in each test case, but let’s focus on the structure here for now.
This code defines a slice of anonymous structs named
testCases. Each element in the slice represents a test case for the
Each test case has several fields that define different aspects of the test. The
desc field contains a string that describes the test case. The
args field contains a pointer to an
args struct that defines the arguments that should be passed to the
result field contains the expected return value of the
VehicleDiagnostics function for this test case. The
on field is a function that sets up the mocked dependencies (i.e. the
navigation. MockedNavigation and
engine. MockedEngine types) for the test case. The
assert field is a function that checks that the mocked dependencies were used as expected during the test. The test cases are used later in the code to test the
Now we’re finally able to run our test cases
This code iterates over the
testCases slice and runs each test case. For each test case, the code creates a new
ElectricVehicle instance by calling the
NewElectricVehicle function and passing it the mocked
navigation. MockedNavigation types.
on field of the test case is not
nil, it calls the
on function to set up the mocked dependencies. Then, it calls the
VehicleDiagnostics function on the
ElectricVehicle instance and passes it the
isOffline values from the
args struct of the test case.
The code then compares the returned error with the
result value of the test case. If the values are not equal, it prints an error message using the
t. Errorf function from the
testing package. Finally, if the
assert field of the test case is not
nil, it calls the
assert function to check the state of the mocked dependencies. Great isn’t it?
Here’s a full example of our table-driven tests
You remember our mocks we wrote at the beginning of our example?
This is of course a very easy example and a mock for the interface is quickly implemented manually. But what if we want to mock larger interfaces? Do we have to write everything by hand? Of course not!
vektra/mockery is a tool for generating mock implementations of Go interfaces. It can be used to generate mocks that can be used in unit tests to simulate the behavior of real components. This allows you to test the behavior of your code without relying on the real components, which can be slow or difficult to set up.
By running mockery with the following command
and passing our interface name
Navigation for example
mockery will auto-generate a mock using the
stretchr/testify/mock package. A new
mock package will be placed into the
navigation package with a
navigation.go file containing our generated code
Table-driven tests are a powerful and flexible testing technique that can help you write more concise, maintainable, and effective tests for your Go code. By defining your test cases as a table of values, you can easily add new test cases and update existing ones without having to modify your test code. This can make it easier to add new test cases and maintain your tests over time.
Using mocks in your table-driven tests can also provide several benefits. By mocking the dependencies of the code you are testing, you can control the behavior of those dependencies and test how your code responds to different inputs and situations. This can help you test error cases or unusual behavior that might be difficult or impossible to test with the real dependencies. Additionally, using mocks can make your tests run faster and more reliably, since you don’t have to rely on external resources or external code. Overall, using table-driven tests with mocks can help you write more effective and efficient tests for your Go code.
testing library in combination with
vektra/mockery we can write powerful table-driven tests with generated mocks for our components without a lot of effort.
You can find all example code on my GitHub profile here