Mocking Golang with Interfaces In Real Life
// Publicado em: 8 de outubro de 2019Authentication code usually results in an IO call, like reading a file or making a network request. These operations are villains in the unit test world ‘cause they create external dependencies. Imagine you are at the airport unable to run your tests without an Internet connection. Or if your CI system stops working due to lack of permissions to write a specific file to a specific place, halting the whole pipeline.
Writing reliable unit tests means avoiding IO operations like the plague.
BUT we can’t prevent our code to use external resources. Cat pics need to stored somewhere. How should unit tests run without spreading its tentacles around the network?
One answer is mocking. Mocks are objects created to mimic the behavior of real objects in a controlled way. As an example, if the Authenticate(user User) bool
function executes a remote call to fulfill its purpose, it can be substituted by a custom mock during the test with controlled logic and output. To test the system reaction in a failed login, it just returns false
.
In a statically typed language like Golang, is there a way to change functions on the fly? 🤔
The composite answer is separation of concerns and dependency injection – mainly using interface
– where IO bound code is isolated as much as possible and injected into functions or methods that depend on it.
Come on, let me show you in real life.
Mocking zmb3/spotify Authentication
Last week I was working on a side project using Spotify’s API and the zmb3/spotify client. Its authentication method presented a perfect playground to demonstrate mocking.
Let’s say we want to get a playlist’s name from an ID. Here’s the simplest way to do it:
// main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/zmb3/spotify"
"golang.org/x/oauth2/clientcredentials"
)
func main() {
config := &clientcredentials.Config{
ClientID: os.Getenv("SPOTIFY_ID"),
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
TokenURL: spotify.TokenURL,
}
token, err := config.Token(context.Background())
if err != nil {
log.Fatalf("couldn't get token: %v", err)
}
client := spotify.Authenticator{}.NewClient(token)
const PLAYLIST_ID spotify.ID = "4OyKDT6cLw96G7bd8nTfxD"
results, err := client.GetPlaylist(PLAYLIST_ID)
if err != nil {
log.Fatalf("couldn't get playlist: %v", err)
}
fmt.Println(results.Name)
}
So far, nothing is being mocked. To run the code above you will need a pair of Spotify’s API key and secret, loaded in the environment variables:
$ export SPOTIFY_ID=<your id>
$ export SPOTIFY_SECRET=<your secret>
Running it:
$ go run main.go
Brunchies
🎉!
By the way, Brunchies is a real playlist. The very best one.
Separation of Concerns
The code is quite convoluted. A single main()
function is doing all the work. Unit testing becomes hard. Hell, impossible. A call to main()
in the test suit will do multiple network requests to Spotify’s API right away.
Let’s break the code apart:
// main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/zmb3/spotify"
"golang.org/x/oauth2/clientcredentials"
)
func main() {
const PLAYLIST_ID spotify.ID = "4OyKDT6cLw96G7bd8nTfxD"
client := newSpotifyClient()
name := getPlaylistName(client, PLAYLIST_ID)
fmt.Println(name)
}
func newSpotifyClient() *spotify.Client {
config := &clientcredentials.Config{
ClientID: os.Getenv("SPOTIFY_ID"),
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
TokenURL: spotify.TokenURL,
}
token, err := config.Token(context.Background())
if err != nil {
log.Fatalf("couldn't get token: %v", err)
}
client := spotify.Authenticator{}.NewClient(token)
return &client
}
func getPlaylistName(client *spotify.Client, playlistID spotify.ID) string {
result, err := client.GetPlaylist(playlistID)
if err != nil {
log.Fatalf("couldn't get playlist: %v", err)
}
return result.Name
}
Nice! Two distinct methods were defined:
newSpotifyClient()
authenticates and returns a client to be used in subsequent operations, like getting playlist informationgetPlaylistName()
well, gets a playlist name 🤷♀️
Note: the later accepts a *spotify.Client
to do the Spotify request, this is the beginning of the dependency injection pattern. The client is being injected into the function that will use it. Could we inject a mock one? 🤔
Before answering the question, take a look at this simple test:
// main_test.go
package main
import (
"testing"
"github.com/zmb3/spotify"
)
func Test_NewGetPlaylistName(t *testing.T) {
os.Setenv("SPOTIFY_ID", "fake id")
os.Setenv("SPOTIFY_SECRET", "fake secret")
client := newSpotifyClient()
name := getPlaylistName(client, "whatever")
if name != "whatever" {
t.Errorf("expected %s, got %s", "whatever", name)
}
}
Run it:
$ go test
2019/10/07 12:36:23 couldn't get token: oauth2: cannot fetch token: 400 Bad Request
Response: {"error":"invalid_client","error_description":"Invalid client"}
exit status 1
The program returns an error, trying to authenticate with our fake SPOTIFY_ID
and SPOTIFY_SECRET
. We don’t want that.
Interfaces to the rescue!
Imagine we could create a mock for client spotify.Client
with the same GetPlaylist()
method and signature and inject it into in the getPlaylistName()
method during the test.
Since Golang is statically typed – and with the current implementation – only *spotify.Client
can be passed into getPlaylistName()
. It needs to be refactored to accept any type with a GetPlaylist()
method – like a mock client.
Fortunately, Golang has the concept of interface
: it is a type with a set of methods signatures. If any type implements those methods, it satisfies the interface and can be recognized by the interface’s type. This is a very simple explanation, you can get some more depth here.
The code below shows the interface implementation:
// main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/zmb3/spotify"
"golang.org/x/oauth2/clientcredentials"
)
// 1
type spotifyClient interface {
GetPlaylist(playlistID spotify.ID) (*spotify.FullPlaylist, error)
}
func main() {
const PLAYLIST_ID spotify.ID = "4OyKDT6cLw96G7bd8nTfxD"
client := newSpotifyClient()
name := getPlaylistName(client, PLAYLIST_ID)
fmt.Println(name)
}
func newSpotifyClient() *spotify.Client {
config := &clientcredentials.Config{
ClientID: os.Getenv("SPOTIFY_ID"),
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
TokenURL: spotify.TokenURL,
}
token, err := config.Token(context.Background())
if err != nil {
log.Fatalf("couldn't get token: %v", err)
}
client := spotify.Authenticator{}.NewClient(token)
return &client
}
// 2
func getPlaylistName(client spotifyClient, playlistID spotify.ID) string {
result, err := client.GetPlaylist(playlistID)
if err != nil {
log.Fatalf("couldn't get playlist: %v", err)
}
return result.Name
}
- 1: Creates the interface with the same
GetPlaylist()
signature fromspotify.Client
- 2: Accepts the
client
parameter using thespotifyClient
interface type
Cool! I promise that everything still works 😅, just run go run main.go
.
Back to the tests. Now, we can create a mockSpotifyClient
struct implementing the GetPlaylist()
method with a controlled output. Since it satisfies the spotifyClient
interface, it can be used in the getPlaylistName()
function.
// main_test.go
package main
import (
"testing"
"github.com/zmb3/spotify"
)
// 1
type mockSpotifyClient struct{}
// 2
func (m *mockSpotifyClient) GetPlaylist(playlistID spotify.ID) (*spotify.FullPlaylist, error) {
// 3
return &spotify.FullPlaylist{
SimplePlaylist: spotify.SimplePlaylist{
Name: "whatever",
},
}, nil
}
func Test_NewGetPlaylistName(t *testing.T) {
// 4
client := &mockSpotifyClient{}
// 5
name := getPlaylistName(client, "whatever")
if name != "whatever" {
t.Errorf("expected %s, got %s", "whatever", name)
}
}
Step by step:
- 1: Creates an empty
struct
- 2: Satisfies its interface by attaching a
GetPlaylist()
method to it - 3: Returns a controlled input, already expected by our test
- 4: Instantiates the
mockSpotifyClient
, no need for fake API keys anymore - 5: Since
client
implements thespotifyClient
interface, it can be injected. When executed inmain.go
, it will return our fake playlist ✨
The proof:
$ go test
PASS
Isn’t it awesome? 🎉
Here are some tips to write code like this:
- If you are still experimenting – or have a sandbox API – write the tests actually using the API to see the basics working
- Once you’ve identified how where the IO operations happen, isolate them, like in
getSpotifyClient()
- After understanding which methods your “client” uses, abstract them in an interface
- If you have multiple “clients” – for Spotify, AWS, etc – create an
Env
holding all the interfaces, something like the following:
// env.go
type Env struct {
spotifyClient spotifyClientInterface
awsClient awsClientInterface
}
func (e *Env) Load() {
// initialize all the clients
}
// aws.go
func awsCall(client awsClientInterface) {}
// main.go
e := Env{}
e.Load()
awsCall(e.awsClient)
Then, in the unit tests, just mock whatever awsClientInterface
expects and voilà.