3.3 Mocking
This lesson builds upon the previous Calling APIs lesson and describes how we can write unit tests for code that normally requires an internet connection and a real application server to respond to each request. Mocking these API calls is a helpful tool that adds test coverage to your application to increase confidence in your implementation.
What is a mock?
A mock is an instance of some entity that is used to test a specific piece of functionality in an application. In this case, mocking is the practice of creating and using mock objects in unit tests.
In order for mock tests to be worthwhile, the mock object must be a valid instance of the component it replaces. This puts a key constraint on the mock object: the mock must be able to be exchanged with the real object without adjusting the implementation.
Interfaces in Go
Mocks can be a difficult concept to understand at first, so it will help to look
at some examples. In Go, the interface is the language feature that we'll
need to use in order to create mocks. Recall that an interface is an abstract
definition for a collection of method signatures. This is similar to the concept
of object-oriented programming interfaces, which are common in a variety of
programming languages, such as Java.
In the following example, the Cat and Dog struct types implement the Pet
interface. Note that both the Cat and Dog types implement more methods than is
specified by the Pet interface; in this case the Pet interface is a subset
of the functionality that each of these struct types are capable of.
Now, if I have another function that simply requires a Pet, but not specifically
a Cat or a Dog, then I can write a function where both a Cat or a Dog
can be used interchangeably. For example, consider the following PetStore type:
Mocks in Go
Now that we're more familiar with the interface type, we can create a mock
for an entity that is otherwise difficult to reliably test.
We can build upon the example described above by extending the PetStore so that it
writes Pet names with a third-party, custom external.Store type. Note that we do
not own the external.Store type, so we can't adjust it to our needs without creating
a fork.
The
external.Storetype above is intentionally simplified for educational purposes and is not representative of a production-ready implementation of anio.Writer.
Now if we add the external.Store type to the PetStore, we can write the AddPet
method:
Given this structure, how could we reliably test the PetStore implementation to
see if anything was written to the external.Store type? We don't have access to the
content attribute in the external.Store type because it's un-exported, and the
external.Store API doesn't expose anything else for us to inspect it.
In this case, we can refactor our code so that the external.Store attribute is written
as an interface and mock it!
The AddPet implementation can remain the same (since the Store type implements the io.Writer
interface), and we can create a separate mock object for unit tests that can be more reliably
tested. For example, consider the following pet_store_test.go file:
With this, we're now confident that the PetStore implementation is interacting
with the io.Writer attribute as we expect. Although we're not testing the
external.Store directly, the interaction between the PetStore and
external.Store is worthwhile.
Testing HTTP RPCs
It's very important to write tests for RPCs, but it's normally difficult to do this reliably since RPCs require a network connection. Fortunately, the standard library actually provides a httptest package that helps with this exactly.
You'll be interacting with this package more in the next assignment!