Using Go Interfaces for Testable Code
Interfaces are abstractions that define the behavior of a particular type but do not specify the details of how that behavior is implemented. If you think of a bank teller, you understand that you can request and receive money from them (provided you have the money in your bank account of course). Whether the teller is an employee at a bank branch or the ATM at your local deli the outcome defined by the “teller” interface remains the same. You can still request and receive money even when the particular mechanics differ between the person and the machine implementations.
Many object-oriented programming languages have some notion of interfaces. Go’s interfaces are particularly interesting and distinctive because they are satisfied implicitly. This means that instead of specifying that the object you are creating implements a particular interface, the Go compiler figures that out for you. If your object contains all the methods defined by an interface Go understands that your object implements that interface and can be used wherever that interface is used.
So why is that helpful and how can we use interfaces to write testable code?
Case: Using External Libraries
A common scenario that illustrates the utility of interfaces is testing code which uses an external client library to do something; in this case to get data from an imaginary web API.
Imagine we have an
external package that is a library written to interact with an external web service. It would likely export a
Client object with methods to interact with the API. Imagine a
GetData method which in this case can only return data but could make a web request and theoretically return an error if this was a real implementation.
Here we have package
foo which is the code we are writing that wishes to use the
Client to get data from the external API and then do things with it. We have two possible error cases in this implementation — one in which
GetData returns an error, perhaps because the web request failed, and another in which the data returned was not what we expected and we therefore can not process it. Both paths will result in our
Controller function returning an error.
Now let’s take a look at how we would test the
Controller function. We could have two basic tests, one which tests the success of the function and one which tests the two failure cases. The problem is that we cannot influence the behavior of the external API and therefore cannot force
GetData to succeed or fail.
The above test
TestController_Success will pass but
TestController_Failure will not because of our inability to test the failure cases. This is confirmed and illustrated in the coverage report.
Not only are our failure cases uncovered but our unit tests are now non-deterministic, at the mercy of the external API to fail or succeed at will. We need a way to stub the behavior of
GetData in our code so that we can manipulate its output during our unit tests and that is where interfaces become very useful.
Using Interfaces To Enable Stubbing
If we can define an interface that the external library’s
Client object satisfies, then we can use that interface in our code instead. This would allow us to feed in a fake client object during testing and the real one during normal operation.
In other languages this would require us to modify the external library code to explicitly state that
Client implemented our new interface (oh no!), but since Go interfaces are satisfied implicitly the compiler will already know that! Yay!
So here we define the
IExternalClient interface (Go purists please don’t crucify my naming) that specifies the methods that we use in our foo
Controller and we modify the controller function to take an
IExternalClient interface type as a parameter. The rest of the controller function then operates as it was before, calling the
GetData method of the interface rather than the specific external
Now let’s take a look at how much easier it is to test our
Controller method. We can implement the
IExternalClient interface by simply implementing the
GetData method on our
MockClient object, but in our
MockClient implementation we have it return what we want it to return. We then feed in our implementations of
Controller in our tests. We can use
MockClient to return different values for the result of
GetData and our
FailingClient to have
GetData return an error.
We are now easily able to handle all the branches of our
Controller function as the updated coverage report confirms.
As you can see, writing and using interfaces can increase the flexibility and testability of your code, and can be an easy solution for stubbing external dependencies.