From 89581e1a9335d2b6b758f1a30b4bdacdeb2442bf Mon Sep 17 00:00:00 2001 From: Liam Fallon <35595825+liamfallon@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:08:11 +0000 Subject: [PATCH] Unit Test Mocking using Mockery (#114) This is a contributor guide describing how to use Mockery. It draws heavily on [PR-441](https://github.com/nephio-project/nephio/pull/441) raised by @vjayaramrh --------- Co-authored-by: Victor Morales --- .../docs/guides/contributor-guides/_index.md | 8 +- .../contributor-guides/minimal-environment.md | 2 +- .../unit-testing-mockery.md | 228 ++++++++++++++++++ 3 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 content/en/docs/guides/contributor-guides/unit-testing-mockery.md diff --git a/content/en/docs/guides/contributor-guides/_index.md b/content/en/docs/guides/contributor-guides/_index.md index 79a31d8f..1766843b 100644 --- a/content/en/docs/guides/contributor-guides/_index.md +++ b/content/en/docs/guides/contributor-guides/_index.md @@ -7,9 +7,5 @@ weight: 3 # Developer guide -This developer guide is for for people who want to write code for the Nephio project. This document is written as an -extension of the [Kubernetes Developer Guides](https://github.com/kubernetes/community/tree/master/contributors/devel#the-process-of-developing-and-contributing-code-to-the-kubernetes-project) -and therefore covers topics that are specific to Nephio development. - -* How to set up a [Minimal Development Environment](/content/en/docs/guides/contributor-guides/minimal-environment.md) defines common terminology - used in the Nephio project. \ No newline at end of file +This developer guide is for people who want to write code for the Nephio project. This document is written as an extension of the [Kubernetes Developer Guides](https://github.com/kubernetes/community/tree/master/contributors/devel#the-process-of-developing-and-contributing-code-to-the-kubernetes-project) +and therefore covers topics specific to Nephio development that are not already covered there. diff --git a/content/en/docs/guides/contributor-guides/minimal-environment.md b/content/en/docs/guides/contributor-guides/minimal-environment.md index f173b5f3..81ef4be5 100644 --- a/content/en/docs/guides/contributor-guides/minimal-environment.md +++ b/content/en/docs/guides/contributor-guides/minimal-environment.md @@ -1,7 +1,7 @@ --- title: Minimal Environment install for development description: > - Minimal Environment install for development + How to set up a minimal environment for development in the Nephio project. weight: 5 --- diff --git a/content/en/docs/guides/contributor-guides/unit-testing-mockery.md b/content/en/docs/guides/contributor-guides/unit-testing-mockery.md new file mode 100644 index 00000000..9ef583e3 --- /dev/null +++ b/content/en/docs/guides/contributor-guides/unit-testing-mockery.md @@ -0,0 +1,228 @@ +--- +title: Unit Test mocking using Mockery +description: > + How to set up and use Mockery for unit testing in Nephio. +weight: 5 +--- +# Introduction + +This guide describes how to use [testify](https://pkg.go.dev/github.com/stretchr/testify) and [mockery](https://vektra.github.io/mockery/latest/) 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](https://vektra.github.io/mockery/latest/#why-mockery) 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](https://github.com/nephio-project/nephio/blob/main/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](https://github.com/vektra/mockery/blob/master/docs/configuration.md) section of the Mockery documentation. + +## Example 1 +Here, we use the [Nephio Controllers package .mockery.yaml](https://github.com/nephio-project/nephio/blob/main/controllers/pkg/.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](https://github.com/vektra/mockery/blob/master/docs/configuration.md) 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](https://github.com/nephio-project/nephio/blob/main/controllers/pkg/giteaclient/mock_GiteaClient.go) 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](https://github.com/nephio-project/nephio/tree/main/testing/mockeryutils) 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](https://github.com/nephio-project/nephio/blob/main/testing/mockeryutils/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](https://github.com/nephio-project/nephio/blob/main/testing/mockeryutils/mockeryutils-types.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](https://github.com/nephio-project/nephio/blob/main/controllers/pkg/reconcilers/repository/reconciler_test.go) 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.