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.
About table-driven tests
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.
So why should I use mocks?
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 take a look at a simple example
Let’s look at the following project structure:
|
|
A vehicle has to components navigation
and engine
. Both dependencies must be mocked in order to test the vehicles
VehicleDiagnostics()
function.
|
|
This code defines a Go package named vehicle
that contains the implementation of an ElectricVehicle
type. The ElectricVehicle
type has two fields: engine
and navigation
. The engine
field is of type engine.Engine
, and the navigation
field is of type navigation.Navigation
. These types are defined in the engine
and navigation
packages, respectively.
The package also defines two global variables: errLowBattery
and 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
.
The 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 nil
otherwise.
Getting started with stretch/testify
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 mock.Mock
type.
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 vehicle
package.
|
|
We’ll start by introducing new structs which we’ll need for our table tests.
|
|
The code defines two new types: mockedFields
and args
. The mockedFields
type is a struct (i.e. a composite data type) that has two fields: navigation
and engine
. The 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 navigation
and engine
packages, respectively.
The args
type is also a struct, and it has two fields: lowBatteryThreshold
and isOffline
. The 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 VehicleDiagnostics
function.
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 VehicleDiagnostics
function.
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 VehicleDiagnostics
function. 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 VehicleDiagnostics
function.
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 engine.MockedEngine
and navigation. MockedNavigation
types.
If the 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 lowBatteryThreshold
and 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
|
|
Are you too lazy to write mocks?
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
|
|
Let’s wrap it up
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.
With Go’s testing
library in combination with stretch/testify
and 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