From 53799973f0f3a2bcdda7ec151a7fa28dc20d89a4 Mon Sep 17 00:00:00 2001 From: Selene Date: Wed, 9 Mar 2022 14:08:55 +0100 Subject: [PATCH] Documentation: Add using mock explanation for testing (#46360) * Add using mock explanation for testing * Update information of how to add go:generate in code Co-authored-by: Marcus Efraimsson Co-authored-by: Marcus Efraimsson --- contribute/style-guides/backend.md | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/contribute/style-guides/backend.md b/contribute/style-guides/backend.md index 351f317c812..8783e69189a 100644 --- a/contribute/style-guides/backend.md +++ b/contribute/style-guides/backend.md @@ -58,6 +58,100 @@ code, plus lets you run each test case in isolation when debugging. Don't use `t Use [`t.Cleanup`](https://golang.org/pkg/testing/#T.Cleanup) to clean up resources in tests. It's a less fragile choice than `defer`, since it's independent of which function you call it in. It will always execute after the test is over in reverse call order (last `t.Cleanup` first, same as `defer`). +### Mock + +Optionally, we use [`mock.Mock`](https://github.com/stretchr/testify#mock-package) package to generate mocks. This is +useful when you expect different behaviours of the same function. + +#### Tips + +- Use `Once()` or `Times(n)` to make this mock only works `n` times. +- Use `mockedClass.AssertExpectations(t)` to guarantee that the mock is called the times asked. + - If any mock set is not called or its expects more calls, the test fails. +- You can pass `mock.Anything` as argument if you don't care about the argument passed. +- Use `mockedClass.AssertNotCalled(t, "FunctionName")` to assert that this test is not called. + +#### Example + +This is an example to easily create a mock of an interface. + +Given this interface: + +```go +func MyInterface interface { + Get(ctx context.Context, id string) (Object, error) +} +``` + +Mock implementation should be like this: + +```go +import + +func MockImplementation struct { + mock.Mock +} + +func (m *MockImplementation) Get(ctx context.Context, id string) error { + args := m.Called(ctx, id) // Pass all arguments in order here + return args.Get(0).(Object), args.Error(1) +} +``` + +And use it as the following way: + +```go + +objectToReturn := Object{Message: "abc"} +errToReturn := errors.New("my error") + +myMock := &MockImplementation{} +defer myMock.AssertExpectations(t) + +myMock.On("Get", mock.Anything, "id1").Return(objectToReturn, errToReturn).Once() +myMock.On("Get", mock.Anything, "id2").Return(Object{}, nil).Once() + +anyService := NewService(myMock) +resp, err := anyService.Call("id1") + +assert.Equal(t, resp.Message, objectToReturn.Message) +assert.Error(t, err, errToReturn) + +resp, err = anyService.Call("id2") +assert.Nil(t, err) +``` + +#### Mockery + +When an interface to test is too big, it's annoying to mock each function manually. To avoid this, you can +use [`mockery`](https://github.com/vektra/mockery) library to generate the mocks. + +The command is like the following (there are more options documented if you need to use another one): + +``` +mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go +``` + +- `--name`: Interface to mock +- `--structname`: Mock implementation name +- `--inpackage`: To use the same package name as the interface +- `--filename`: Your mock generated file name + +If any interface signature changes, executing the command again updates the mock. + +Additionally, you can put `go:generate` command on the top of the file as a comment. It's useful because some IDEs +like Goland and Visual Studio Code allows executing scripts from the IDE. + +``` +package + +import ( + ... +) + +//go:generate mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go +``` + ## Globals As a general rule of thumb, avoid using global variables, since they make the code difficult to maintain and reason