Unit Test mocking using Mockery
This guide describes how to use testify and mockery for writing, mocking, and executing unit tests. This guide will help folks come up to speed on using testify and mockery.
How Mockery works
The
mockery documentation describes why you would use and how to use Mockery. In a nutshell, Mockery generates mock implementations for interfaces in go
, which you can then use instead of real implementations when unit testing.
Mockery support in Nephio make
The make
files in Nephio repos containing go
code have targets to support mockery.
The
default-mockery.mk file in the root of Nephio repos is included in Nephio make
runs.
There are two targets in default-mockery.mk:
1. install-mockery: Installs mockery in docker or locally if docker is not available
2. generate-mocks: Runs generation of the mocks for go interfaces
The targets above must be run explicitly.
Run make install-mockery
to install mockery in your container runtime (docker, podman etc) or locally if you have no container runtime running. You need only run this target once unless you need to reinstall Mockery for whatever reason.
Run make generate-mocks
to generate the mocked implementation of the go interfaces specified in ‘.mockery.yaml’ files. You need to run this target each time an interface that you are mocking changes or whenever you change the contents of a .mockery.yaml
file. You can run make generate-mocks
in the repo root to generate or re-generate all interfaces or in subdirectories containing a Makefile
to generate or regenerate only the interfaces in that subdirectory and its children.
The generate-mocks target looks for .mockery.yaml
files in the repo and it runs the mockery mock generator on each .mockery.yaml
file it finds. This has the nice effect of allowing .mockery.yaml
files to be in either the root of the repo or in subdirectories, so the choice of placement of .mockery.yaml
files is left to the developer.
The .mockery.yaml file
The .mockery.yaml
file specifies which mock implementations Mockery should generate and also controls how that generation is performed. Here we just give an overview of mockery.yaml
. For full details consult the
configuration section of the Mockery documentation.
Example 1
Here, we use the Nephio Controllers package .mockery.yaml file as an example:
1. packages:
2. github.com/nephio-project/nephio/controllers/pkg/giteaclient:
We provide a list of the packages for which we want to generate mocks. In this example, we only have one package. Here we want to generate mocks for the GiteaClient interface so we provide the package path to the interface.
3. interfaces:
4. GiteaClient:
5. config:
6. dir: "{{.InterfaceDir}}"
We want mocks to be generated for the GiteaClient
go interface (line 4). The {{.InterfaceDir}}
parameter (line 6) asks Mockery to generate the mock file in the same directory as the interface is located.
Example 2
This example is a slightly more evolved version of the file used in Example 1 above.
1. with-expecter: true
Generate EXPECT() methods for your mocks, see the configuration section of the Mockery documentation.
2. packages:
3. github.com/nephio-project/nephio/controllers/pkg/giteaclient:
4. interfaces:
5. GiteaClient:
6. config:
7. dir: "{{.InterfaceDir}}"
Lines 2 to 7 are as explained in Example 1 above.
8. sigs.k8s.io/controller-runtime/pkg/client:
Generate mocks for the external package sigs.k8s.io/controller-runtime/pkg/client
.
9. interfaces:
10. Client:
Generate a mock implementation of the go interface Client
in the external package sigs.k8s.io/controller-runtime/pkg/client
.
11. config:
12. dir: "mocks/external/{{ .InterfaceName | lower }}"
13. outpkg: "mocks"
Create the mocks for the Client
interface in the mocks/external/client
directory and cal the output package mocks
.
The generated mock implementation
This mocked implementation of the GiteaClient interface was generated by mockery using the make generate-mocks
make target.
We can treat this generated file as a black box and we do not have to know the details of the contents of this file to write unit tests.
The Mockery Utils package
The mockery utils package is a utility package that you can use to initialize your mocks and to define some common fields for your tests.
mockeryutils-types.go contains the MockHelper
struct, which allows you to control the behaviour of a mock.
type MockHelper struct {
MethodName string // The mocked method name for which we want to supply configuration
ArgType []string // The arguments we are supplying to the mocked method
RetArgList []interface{} // The arguments we want the mocked method to return to us
}
The MockHelper
struct is used to configure a mocked method to expect and return a certain set of arguments. We pass instances of this struct to the mocked interface during tests.
mockeryutils.go contains the InitMocks
function, which initializes your mocks for you before a test.
func InitMocks(mocked *mock.Mock, mocks []MockHelper)
For the given mocked
interface, the function initializes the mocks
as specified in the given MockHelper
array.
Using the mock implementation in unit tests
The unit tests for the Repository Reconciler use the mocks generated above.
type fields struct {
APIPatchingApplicator resource.APIPatchingApplicator
giteaClient giteaclient.GiteaClient
finalizer *resource.APIFinalizer
l logr.Logger
}
type args struct {
ctx context.Context
giteaClient giteaclient.GiteaClient
cr *infrav1alpha1.Repository
}
type repoTest struct {
name string
fields fields
args args
mocks []mockeryutils.MockHelper
wantErr bool
}
The code above allows us to specify input data and the expected outcome for tests. Each test is specified as an instance of the repoTest
struct. For each test, we specify its fields and arguments, and specify the mocking for the test.
func TestUpsertRepo(t *testing.T)
This method contains unit tests for the upsertRepo method written using mockery and testify.
tests := []repoTest{}
This is the specification of an array of tests that we will run.
{
name: "Create repo: cr fields not blank",
fields: fields{resource.NewAPIPatchingApplicator(nil), nil, nil, log.FromContext(context.Background())},
args: args{
nil,
nil,
&infrav1alpha1.Repository{
Spec: infrav1alpha1.RepositorySpec{
Description: &dummyString,
Private: &dummyBool,
IssueLabels: &dummyString,
Gitignores: &dummyString,
License: &dummyString,
Readme: &dummyString,
DefaultBranch: &dummyString,
TrustModel: &dummyTrustModel,
},
},
},
mocks: []mockeryutils.MockHelper{
{MethodName: "GetMyUserInfo", ArgType: []string{}, RetArgList: []interface{}{&gitea.User{UserName: "gitea"}, nil, nil}},
{MethodName: "GetRepo", ArgType: []string{"string", "string"}, RetArgList: []interface{}{&gitea.Repository{}, nil, fmt.Errorf("repo does not exist")}},
{MethodName: "CreateRepo", ArgType: []string{"gitea.CreateRepoOption"}, RetArgList: []interface{}{&gitea.Repository{}, nil, nil}},
},
wantErr: false,
}
The code above specifies a single test and is an instance of the tests
array. We specify the fields, arguments, and mocks for the test. In this case, we mock three functions on our GiteaClient interface: GetMyUserInfo
, GetRepo
, and CreateRepo
. We specify the arguments we expect for each function and specify what the function should return if it receives correct arguments. Of course, if the mocked function receives incorrect arguments, it will report an error. The wantErr
value indicates if we expect the upsertRepo
function being tested to succeed or fail.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &reconciler{
APIPatchingApplicator: tt.fields.APIPatchingApplicator,
giteaClient: tt.fields.giteaClient,
finalizer: tt.fields.finalizer,
}
initMockeryMocks(&tt)
if err := r.upsertRepo(tt.args.ctx, tt.args.giteaClient, tt.args.cr); (err != nil) != tt.wantErr {
t.Errorf("upsertRepo() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
The code above executes the tests. We run a reconciler r
and initialize our tests using the local initMockeryTests()
function. We then call the upsertRepo
function to test it and check the result.
func initMockeryMocks(tt *repoTest) {
mockGClient := new(giteaclient.MockGiteaClient)
tt.args.giteaClient = mockGClient
tt.fields.giteaClient = mockGClient
mockeryutils.InitMocks(&mockGClient.Mock, tt.mocks)
}
The initMockeryMocks
local function calls the mockeryutils.InitMocks
to initialize the mocks for the tests.