Golang Testing: Mocking and Error Handling

Learning to plan for testability using Go’s interfaces and built-in error handling

Photo by Boston Public Library on Unsplash

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

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 Jest for JavaScript or 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 jest library.

In the first section, we’ll create the package maintenance that will represent as the third-party dependency to another package vehicle. For 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 vehicle package.

If code inspection is how you learn, I also provided the repo below that we’ll be using for the code tutorial:

Creating the maintenance package

Our dependency package called maintenance will have a struct called 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 PutItem() and Query().

In the function signature of NeedsMaintenance(), aside from returning the output decision, we are also returning err. Golang has a built-in error type error with nil as its zero value. We can use this to easily use to return an error. There are other means of testing related topanics (that’s for another post), but this passing of error outputs gives us a handy way of testing errors compared to the try and 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 assert.Equal() method.

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:

Like the 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).

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.

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 maintenance.Specialist.NeedsMaintenance(days_used) with 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 decision and 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 nil value.

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 NeedsMaintenance():

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 testWrapper is 99 and placed 55,

we’ll get the following error:

Summary

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.

R&D Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store