Unit tests in the API using mocks
When writing unit tests there are two main schools of thought:
- Solitary unit tests, where other classes that are called by your class or function under test are substituted with mocks or stubs
- Sociable unit tests, for tests that allow your class or function under tests talking to real collaborators.
I agree with this perspective, that says:
At the end of the day it's not important to decide if you go for solitary or sociable unit tests. Writing automated tests is what's important. Personally, I find myself using both approaches all the time. If it becomes awkward to use real collaborators I will use mocks and stubs generously. If I feel like involving the real collaborator gives me more confidence in a test I'll only stub the outermost parts of my service.
The tests we did in the last section are in fact sociable unit tests as we are testing the API functions but we did not mock the library.
This is OK as the current library is code is very simple, without "awkward" dependencies.
Nevertheless, if calling the library would require, for example, the setup of a database or an external system, it could be harder to implement unit tests and I would prefer to mock the library.
With this in mind, in this section we will do the necessary code changes so we can mock the library when implementing the unit tests for the API.
To do that we need first to revisit what are interfaces in Go.
As described in the Tour of Go:
An interface type is defined as a set of method signatures.
A value of interface type can hold any value that implements those methods.
And
A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword.
For example (taken from here):
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
fmt.Println(t.S)
}
For us to be able to mock the library we need to define an interface. This allows us to have two different implementations, a real one and a mock.
Creating the interface
Open vscode in the learning-go-lib
project.
As each category will provide a different set of functions, we will create an interface per category.
In our current implementation that's the programming
category.
So, inside the programming
folder add a new file named interface.go
with
the following contents, to define the NewUuid
as part of the package
interface:
package programming
type Interface interface {
NewUuid(withoutHyphen bool) string
}
type ProgrammingFunctions struct {
}
Next we need to change the NewUuid
implementation to bind it to the
ProgrammingFunctions
struct. This makes this structure implement the
interface.
To make the struct implement the interface, we need to put it as receiver
of
the function.
Function definition without receiver
:
func NewUuid(withoutHyphen bool) string
The function with receiver
is:
func (pf *ProgrammingFunctions) NewUuid(withoutHyphen bool) string
This is the new code in the uuid.go
file:
package programming
import (
"strings"
"github.com/google/uuid"
)
// NewUuid generates an UUID with the possibility
// to remove the hyphens
func (pf *ProgrammingFunctions) NewUuid(withoutHyphen bool) string { // receiver added
uuidWithHyphen := uuid.New()
if withoutHyphen {
return strings.Replace(uuidWithHyphen.String(), "-", "", -1)
}
return uuidWithHyphen.String()
}
In the uuid_test.go
file we can now apply the changes and observe the
difference on how the library code will be used:
package programming
import (
"testing"
"github.com/stretchr/testify/assert"
)
// creates a instance of the structure to be used as a receiver
var pf ProgrammingFunctions = ProgrammingFunctions{}
func TestNewUuidWithHyphen(t *testing.T) {
uuidWithHyphen := pf.NewUuid(false) // "pf" used as a receiver
assert.Len(t, uuidWithHyphen, 36)
assert.Contains(t, uuidWithHyphen, "-")
}
func TestNewUuidWithoutHyphen(t *testing.T) {
uuidWithHyphen := pf.NewUuid(true) // "pf" used as a receiver
assert.Len(t, uuidWithHyphen, 32)
assert.NotContains(t, uuidWithHyphen, "-")
}
Creating the mock implementation
Mocks in Go need to be implemented, in contrast with unittest.mock
from the
Python standard library,
which removes "the need to create a host of stubs throughout your test suite".
To facilitate the generation of the mocks we are going to use mockery, a mock code auto-generator for Golang.
Download and install mockery.
After execute the following command to generate the stubs:
mockery --all --inpackage --case snake
This will generate a new file named mock_interface.go
in the programming
folder. The contents of this file are:
// Code generated by mockery v1.0.0. DO NOT EDIT.
package programming
import mock "github.com/stretchr/testify/mock"
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
// NewUuid provides a mock function with given fields: withoutHyphen
func (_m *MockInterface) NewUuid(withoutHyphen bool) string {
ret := _m.Called(withoutHyphen)
var r0 string
if rf, ok := ret.Get(0).(func(bool) string); ok {
r0 = rf(withoutHyphen)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
I would like to highlight the following:
- There is a new struct called
MockInterface
- This struct is the receiver of the mock implementation of the
NewUuid
function
With this done, we need to commit, push and tag this new version of the
learning-go-lib
:
git add .
git commit -m "refactor: introduced interfaces and mocks"
git push
git tag -a v0.0.2 -m "v0.0.2"
git push origin v0.0.2
Next we need to change the API.
Changing the API
Open the learning-go-api
project using vscode.
First we need to change the go.mod
to require the new version of the lib.
module github.com/renato0307/learning-go-api
go 1.17
require github.com/gin-gonic/gin v1.7.7
require (
github.com/renato0307/learning-go-lib v0.0.2 // new version
github.com/stretchr/testify v1.7.0
)
// ... continues
Next we need to change the implementation of the API. We want to make the
package programming
to reference only interfaces instead of referencing
specific implementations.
The full version of the programming.go
file is:
package programming
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/renato0307/learning-go-lib/programming"
)
// postUuidOutput is the output of the "POST /programming/uuid" action
type postUuidOutput struct {
UUID string `json:"uuid"`
}
// SetRouterGroup defines all the routes for the programming functions
func SetRouterGroup(p programming.Interface, base *gin.RouterGroup) *gin.RouterGroup {
programmingGroup := base.Group("/programming")
{
programmingGroup.POST("/uuid", postUuid(p))
// Add here more functions in the programming category
}
return programmingGroup
}
// postUuid handles the uuid request.
//
// Reads the "no-hyphens" parameter from the query string to support
// UUIDs without hyphens.
//
// It returns 200 on success.
func postUuid(p programming.Interface) gin.HandlerFunc {
return func(c *gin.Context) {
noHyphensParamValue := c.Query("no-hyphens")
withoutHyphens := noHyphensParamValue == "true"
uuid := p.NewUuid(withoutHyphens)
output := postUuidOutput{UUID: uuid}
c.JSON(http.StatusOK, output)
}
}
Let me highlight the changes (check the image below):
- The
SetRouterGroup
methods gets receives the interface for the library package - The interface is passed to the
postUuid
function - The
postUuid
receives the interface - The interface is used to call the
NewUuid
function
With this, the programming package no longer references a specific implementation. It references only interfaces.
Next we need to fix the main.go
.
The changes are simple:
- The real implementation is attached to the
ProgrammingFunctions
struct - We need to send an instance to the
SetRouterGroup
package main
import (
"github.com/gin-gonic/gin"
"github.com/renato0307/learning-go-api/programming"
programminglib "github.com/renato0307/learning-go-lib/programming" // new
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, welcome to the learning-go-api",
})
})
base := r.Group("/v1")
p := programminglib.ProgrammingFunctions{} // new
programming.SetRouterGroup(&p, base) // change
r.Run()
}
The only missing step is to use the MockInterface
in the tests, instead of
a real implementation.
The most relevant changes in the programming_test.go
file are:
- The
setupGin
function must receive amockInterface
and pass it to theSetRouterGroup
- The test must instantiate the
mockInterface
to define and assert the expectations - The
mockCall := mockInterface.On("NewUuid", false)
states theNewUuid
function must be called with thefalse
argument. - The
mockCall.Return("1ce44be5-fe68-46f7-a153-51c1c91a4ae4")
defines the return value of theNewUuid
call - The
mockInterface.AssertExpectations(t)
asserts theNewUuid
method was called correctly
func setupGin(mockInterface *programminglib.MockInterface) *gin.Engine {
r := gin.Default()
v1 := r.Group("/v1")
SetRouterGroup(mockInterface, v1)
return r
}
func TestPostUuid(t *testing.T) {
// arrange
mockInterface := programminglib.MockInterface{}
mockCall := mockInterface.On("NewUuid", false)
mockCall.Return("1ce44be5-fe68-46f7-a153-51c1c91a4ae4")
r := setupGin(&mockInterface)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/programming/uuid", nil)
// act
r.ServeHTTP(w, req)
// assert
assert.Equal(t, w.Code, http.StatusOK)
output := postUuidOutput{}
err := json.Unmarshal(w.Body.Bytes(), &output)
assert.Nil(t, err)
assert.Len(t, output.UUID, 36)
assert.Contains(t, output.UUID, "-")
mockInterface.AssertExpectations(t)
}
🏋️♀️ CHALLENGE: fix the other test in the file and make everything green.
Next
The next section is GitHub actions running locally.