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 Client
implementation.
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 IExternalClient
to 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.
The Go standard libraries use interfaces heavily and you can find great examples in packages like io and net/http.
Happy Coding!