Learning to plan for testability using Go’s interfaces and built-in error handling
The article about Golang test-driven development would have introduced you to the basic concepts for creating unit tests in Go. This time, you now want to learn more about testing practices such as mocking and error handling. If this sounds like you, well you’re in the right place. By the end of this tutorial, hopefully, you would have learned the following:
- planning for testability in Go
- decoupling packages and third-party dependencies using Go interfaces
- using receiver functions and preferred Go mocking patterns
- using Go’s built-in error handling instead of try-catch
Golang Testing with TDD
Learning the basics of Go with a test-driven development approach
Handling Third-Party Dependencies
After delving more time in Go, you will likely be using third-party dependencies, and you might scratch your head again on how to plan for testability this time. Yes, you know how to write unit tests for single independent packages, but how would you write unit tests without explicitly referring to the third-party libraries or dependencies?
There may be instances (quite often, actually) where we cannot use real third-party libraries in testing. Or if we do, it will involve changing the intention of of creating unit tests into the bulky integration tests. For example, you may want to use the read and write capability of a database like dynamoDB. In unit testing, do we need to call a real dynamoDB? Not really. We’ll help address how you can “skip” this part using mocks, interfaces and receiver functions in later sections.
By planning for testability too, you’ll also decouple your packages from other packages. Oh, and the advantages do not end there. When we mock third-party dependencies in your unit tests, you can easily test edge cases like error handling.
Note for those not-so-new to mocking:
Golang may not be your first go-to language and you probably have encountered the advantages of mocking. Compared to other testing packages like
MagicMock for Python, mocking in Go may not look as straightforward as it should on the surface. Sure, there’s more lines of code to write in Go for mocks BUT it has the advantage of “owning” the interfaces without depending on somebody else’s libraries. This helps you nicely decouple your packages from third-party packages. There may be hundreds of functions and interfaces in a library say for dynamoDB like in https://github.com/aws/aws-sdk-go-v2/tree/main/service/dynamodb but we’ll only need to mock just those that we’ll need.
Overview of the code tutorial
For this tutorial, we’ll be using a
maintenance package that is the third-party library used by the
vehicle package. We’ll show how we could have done it without mocking and then we’ll look at the recommended way using mocks, interfaces, and receiver functions. By the way, for all the tests in the different sections of the code tutorial, we’ll use the
testify library to make the tests familiar to programmers used to the
In the first section, we’ll create the package
maintenance that will represent as the third-party dependency to another package
maintenance, we will create a function that has an
error type as one of its output arguments, so you can easily see how to do simple and straightforward error handling.
The next section will create the
vehicle package that made no assumptions or plans for testability. We’ll point out what are the disadvantages both from the functional perspective and the testing perspective. Yes, we’ll have tests for this package too but we will also mention why these tests need to be changed.
After that, we’ll refactor the
vehicle package to plan for testability. In doing so, we’ll use concepts of mocking, an
interface, and the use of receiver functions. We’ll also contrasts the unit tests that were made using the decoupled way of creating the
If code inspection is how you learn, I also provided the repo below that we’ll be using for the code tutorial:
- https://github.com/williaminfante/go_test_mocking (
mainbranch) containing mocks, interfaces, and receiver functions where
vehiclepackage is decoupled from the
- https://github.com/williaminfante/go_test_mocking/tree/no-mocks (
no-mocksbranch) does NOT consider mocks, interfaces, and receiver functions where
vehiclepackage is coupled with the
maintenancepackage. Please note that this is not the recommended way and was only created for learning purposes.
Creating the maintenance package
Our dependency package called
maintenance will have a
Specialists that is supposed to contain a bunch of functions. But for this demo, we only defined one:
NeedsMaintenance(). Notice the parallelism of having a third-party package to a database client with multiple methods like
In the function signature of
NeedsMaintenance(), aside from returning the output
decision, we are also returning
err. Golang has a built-in error type
nil as its zero value. We can use this to easily use to return an error. There are other means of testing related to
panics (that’s for another post), but this passing of error outputs gives us a handy way of testing errors compared to the
catch combinations from other languages.
Our simple function just returns an error if the input
days is less than
0 and adds a Boolean decision depending on the number of valid days. In this simple third-party package, we just hard-coded that conditional with the number
30. And just like good ol’ way, we should never forget about tests even for your packages. This sample package, for example, has been created using the test-driven development approach with coverage where your tests may look like the ones below:
Let’s just spend a bit of time on how straightforward testing of errors could be. It may not always be the case (but could be) for functions created in Go. Since the
error is just being passed as another output argument, we can then check the value with just an
Creating the vehicle package — coupled to maintenance
Cool, so you now have your
maintenance package (with the
Specialist struct (with a method like
NeedsMaintenance() )). We can then use
NeedsMaintenance() in a newly created
vehicle package as part of its
ConductChecks() function. In this section, we’ll assume that we don’t know care yet for testability and we will construct our package like so:
maintenance package, we also wanted
ConductChecks()to have an error output argument. We’ll be needing this function signature to demonstrate error handling of mock functions later.
In the code, the
vehicle package imports the
maintenance package. If anything happens to the
maintenance package, we don’t have control on our end (and even the tests for
vehicle). Now, let’s try to create the tests for this and see additional problems from this coupling.
Even for the tests, we need access to the third-party library
maintenance. If we focus on the
t.Run("error" func(t *testing.T), you’ll notice that for us to check all conditions within our
ConductChecks() function, we need to know what values to place inside
NeedsMaintenance() for it to reproduce an
error. The demo example can easily generate an error as we only need to place a negative number, but this is not always the case for third-party methods. We needed a deep internal knowledge, including edge cases, of the third-party library even if this is just supposedly a unit test of
vehicle package (and not
maintenance package). In addition, if we still like to use the
assert.Equal() test, we also need to return the exact error generated by the third-party function. Problematic: coupling in the package itself and the associated tests.
In Golang, I think this StackOverflow post neatly summarizes what are allowed to be mocked as functions. For example, we cannot mock function declarations, but we can mock parameters passed to another function or interfaces (the preferred way).
How to mock functions in golang
In Go you cannot mock a function declaration, same with method declarations on a concrete type, you cannot mock those…
Refactoring the vehicle package — decoupled from maintenance
Now, for refactoring we’ll need interfaces and a bit more knowledge on mocking and receiver functions.
So, this might be your question: what are receiver functions? I find that the article below is quite useful: https://medium.com/@adityaa803/wth-is-a-go-receiver-function-84da20653ca2, or even the information they provide in Golang tours. The link below provides a brief but useful description https://tour.golang.org/methods/4. To recap, receiver functions have methods with pointer receivers that can modify the value to which the receiver points.
What the heck are Receiver functions in Golang?
Go is very similar to C due to presence of pointers, static typing and many other things. But Go is a modern language…
For our case, we must find a way to recreate the signature of
Specialist with its function methods using a struct we’ll call
MaintenanceWrapper that contains
Maintenance which is of type
MaintenanceAPI . Now,
MaintenanceAPI is an
interface showing the signature of the function we’d like to decouple. Just to emphasize here, we did not define how
NeedsMaintenance() is implemented but just what types we are expecting for the input and output arguments.
Afterwards, we can convert
ConductChecks() to have a receiver function based on the
MaintenanceWrapper. Notice now that we have replaced
w.Maintenance.NeedsMaintenance(days_used) where the
vehicle package now is not dependent on the
maintenance package itself!
Below is the refactored code:
And to provide a more holistic picture, if we want to use
ConductChecks() in the main execution, that’s the time when we should not forget to assign the third-party package
maintenance definition of
Specialist to that of the
Maintenance in the
MaintenanceWrapper. For example, we may associate it using the following in the main function.
Okay, so we’ve seen the benefit of decoupling the two packages, but it also has another benefit. We also made the function
ConductChecks() testable as we can just replace the receiver function using
MaintenanceWrapper with a mock wrapper that contains mock functions with the same function signatures.
In a separate file, we could add, in the package
mocks, a mock wrapper that expects us to call the input
days and return two arguments
err . There are multiple ways of implementing our mock functions. For this simple case, we just used an
if statement like
if len(args)>1 to generate the function with and without errors. In this case, if we specify more than one argument in the test later, we’ll return an error too. If not,
err will have a
For the tests, instead of assigning the real definition from the
maintenance package to the interface in
vehicle, we’ll just use the mocked wrapper from
mocks. In contrast to the previous section test, we’ll have the revised tests below:
Even the error that was sent by
NeedsMaintenance() can be mocked to a different value! If we check
t.Run("error" func(t *testing.T) in the test, we can use any error we want like
errors.New("Sample" without the deep knowledge of the third-party function except for its interface.
Just some details on mocking: we used the following to mock the expected input and the expected output for
testWrapper.On("NeedsMaintenance", testDays).Return(false, expectedErr).Once()
We can then use
testWrapper.AssertExpectations(t) to know if the values are the correct. Now, if we create the wrong test for demo purposes where the input the
99 and placed
we’ll get the following error:
This is one of the longer tutorials, but I hope you learned how planning for testability inherently makes your packages decoupled from third-party dependencies. Writing tests makes you write better code.
Although it takes more steps, more concepts (like receiver functions and interfaces) and more files to make mocking work in Go compared to other popular languages, Go mocking can let you “own” the interfaces.
Lastly, we’ve also covered how we can implement error-handling by passing an
error argument instead of the try-catch methods in other popular languages.