Golang’s Interfaces Explained with Mocks
When I was learning Golang, one of the concepts that took me the most time to grasp was Interfaces. Golang’s Interfaces are a great way to make modular and testable code. But they can also be a bit confusing at first glance.
One of the best ways I’ve found to teach how interfaces work is by creating a mock implementation of an existing Interface.
This article will cover creating an interface and creating a mock implementation for that Interface. But before we jump into a working example, we should first understand the value of Interfaces.
Why Interfaces
When thinking about Interfaces, consider them in themselves as contracts. An Interface is a contract that users can import and code their application around. The Interface in itself does nothing; it’s a simple contract with no functionality.
An Interface requires implementations to be useful; an implementation is a Struct with the same Fields and Methods that adhere to the Interface’s contract. To explain this better, we will create a simple Speak
Interface.
From the above, we can see that our Speak
Interface requires implementations to have a Method called SayHello
. By itself, this Interface has no functionality. You can't declare a variable as a Speak
type and run SayHello
. We need an implementation to do this.
The above is an example of an implementation that satisfies the Speak
Interface. In the above, we have a simple Struct named English
; this Struct also has a Method SayHello
. Since this implementation has the same Methods and Fields as our Speak
Interface, we can use it with our Interface.
This example shows just how we can use the English
implementation to fulfill our Interface. We declared our variable voice
to be of the type Speak
, we then set the voice
value to a new English
instance. Now, any time we execute voice.SayHello()
, we will use the English
implementation.
So far, we’ve shown how you can define an Interface and use a single implementation to satisfy that Interface. But none of this shows how we can use Interfaces to create “modular” code. Let’s do that by adding another implementation of our Speak
Interface.
In the above example, we have added a new function NewVoice()
. Based on the user's input, this function will return either a Spanish
or an English
implementation of our Speak
Interface. This example shows just how we can use Interfaces to create modular programs.
As a user of the Interface throughout our application, we can reference the voice
variable and use the contracted Methods. No matter how many further implementations we add, we don't have to change our base code, only what NewVoice()
returns.
Now that we’ve explored the basics of Interfaces and how they can help us write modular code. We can now explore how Interfaces can also help us write testable code.
Mocking a Database Interface
Databases are a ubiquitous example of where Interfaces come in handy. Interfaces’ modular nature allows us to change the underlying database logic or even platform without modifying the application logic.
From a testing perspective, Interfaces give us a simple way to introduce variability into our code. By creating mock implementations of our Database
Interface, we can make testing scenarios where the database provides different results.
This section of the article will first create a simple application that fetches data from a database. We will then create a mock implementation of that database and use it to test how the application behaves with different responses.
The above example application is relatively simple. We have a simple Database
Interface with a single method, Fetch
. We also have an instance of this Interface named DB. DB is used to retrieve database records, and based on the results; our function returns either a True
or a False
.
While this example is simplistic, it’s a great start to show how creating a mock can help test application logic. To get started making our mock, we will start our main_test.go
file. Within this file, we will create our Mock implementation of the Database
Interface.
In this implementation, we can see something a bit more sophisticated from our original example. Our MockDB
Struct has a Field called FakeFetch
, which is a func()
type. What's more interesting is that the FakeFetch
will execute within our Fetch
method.
This setup is something I’ve found useful when creating Mocks. Users who use this MockDB
type to mock DB can define what they want the Fetch
method to return. This part will be more apparent as we create the individual tests. What is more important to call out is the m.FakeFetch(...)
call.
When defining Struct methods, we also define a receiver, m
in this case. Users can use the receiver to reference internal values within the Struct. This use of the m
receiver is what we are doing with FakeFetch
. As the users define the MockDB
instance, they are also creating the FakeFetch
function. As our method executes, the user-defined FakeFetch
is then also called. To understand this concept better, let's take a look at some tests using this MockDB
implementation.
In the above, we can see that each test execution defines a different behavior for the DB.Fetch()
method call. In turn, each of these tests drives different return values from our isOver9000()
function. In one test, we fake a result over 9000
, which causes our isOver9000()
function to return True
.
In another test, we return an error from the database forcing the isOver9000()
function to return False
. These tests show just how easy it can be to use Interface mocks to test application logic and, sometimes, more importantly, error handling within applications.
Summary
With this article, we have explored Golang’s Interfaces’ basics and how we can use them to create both modular and testable code. While the examples may have been rudimentary, they should serve as a good starting point.
For more examples of how we can use Interfaces, I’d suggest checking out one of my side projects, Hord. Hord aims to be a friendly Interface for key/value databases. As part of this package, I created a Mock database driver that makes it easy for Hord users to test application logic driven by database results.
Originally published at https://bencane.com.