Github.com/stretchr/testify: Difference between revisions
(→Mocks) |
|||
Line 54: | Line 54: | ||
The programming model requires to: | The programming model requires to: | ||
* Start with an interface, the mock will implement that interface. | |||
* [[#Defining_the_Mock|define]] such a mock instance | * [[#Defining_the_Mock|define]] such a mock instance | ||
* [[#Instantiate_the_Mock|instantiate]] it with <code>new()</code> or <code>&SomeInterfaceMock{}</code> | * [[#Instantiate_the_Mock|instantiate]] it with <code>new()</code> or <code>&SomeInterfaceMock{}</code> | ||
Line 61: | Line 62: | ||
* [[#Test_with_the_Mock_and_Verify_the_Results|ensure]] the code behave correctly, knowing that the mock returned what we instructed it to return | * [[#Test_with_the_Mock_and_Verify_the_Results|ensure]] the code behave correctly, knowing that the mock returned what we instructed it to return | ||
* optionally, [[#Assert_Expectations|assert expectations]] on the mock. | * optionally, [[#Assert_Expectations|assert expectations]] on the mock. | ||
===Start with an Interface=== | |||
Let's assume that the instances we want to mock are defined by the <code>Something</code> interface, declared as follows: | |||
<syntaxhighlight lang='go'> | |||
package somepkg | |||
type Something interface { | |||
SomeFunc(s string, i int) (string, error) | |||
SomeOtherFunc(s string) (string, error) | |||
} | |||
</syntaxhighlight> | |||
===<span id='Defining_the_Mock'></span>Define the Mock=== | ===<span id='Defining_the_Mock'></span>Define the Mock=== | ||
Line 74: | Line 87: | ||
└── somepkg_mocks_test.go | └── somepkg_mocks_test.go | ||
</font> | </font> | ||
The mock, implemented as the <code>SomethingMock</code> <code>struct</code>, should be defined in the <code>somepkg_mocks_test.go</code> file. | The mock, implemented as the <code>SomethingMock</code> <code>struct</code>, should be defined in the <code>somepkg_mocks_test.go</code> file. |
Revision as of 23:38, 6 March 2024
External
Internal
Overview
Installation
go get github.com/stretchr/testify
Programming Model
Assertions
package yours
import (
"testing"
tassert "github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
assert := tassert.New(t)
// assert equality
assert.Equal(123, 123, "they should be equal")
// assert inequality
assert.NotEqual(123, 456, "they should not be equal")
// assert for nil (good for errors)
assert.Nil(object)
// assert for not nil (good when you expect something)
if assert.NotNil(object) {
// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal("Something", object.Value)
}
}
To check that an error has the expected message:
err := ...
assert.NotNil(err)
assert.Equal(err.Error(), "expected message")
Mocks
The Testify mock
package provides a system by which it is possible to mock objects and verify calls into those objects are happening as expected. Mocking with Testify is based on the assumption that we want to construct mock instances to replace in testing real instances, standing in for external dependencies, defined by interfaces.
The programming model requires to:
- Start with an interface, the mock will implement that interface.
- define such a mock instance
- instantiate it with
new()
or&SomeInterfaceMock{}
- configure its behavior, by configuring its methods' responses to invocations
- pass it to the code that needs to be tested
- run the code that needs to be tested
- ensure the code behave correctly, knowing that the mock returned what we instructed it to return
- optionally, assert expectations on the mock.
Start with an Interface
Let's assume that the instances we want to mock are defined by the Something
interface, declared as follows:
package somepkg
type Something interface {
SomeFunc(s string, i int) (string, error)
SomeOtherFunc(s string) (string, error)
}
Define the Mock
It's a good idea to encapsulate the mock definition(s) in a package-level *_mocks_test.go
file. If we're testing a somepkg
package, then the code lives in the somepkg.go
file, the tests live in somepkg_test.go
file and the mocks live in somepkg_mocks_test.go
:
. └── internal └── somepkg ├── somepkg.go ├── somepkg_test.go └── somepkg_mocks_test.go
The mock, implemented as the SomethingMock
struct
, should be defined in the somepkg_mocks_test.go
file.
The mock struct is a wrapper around the Testify mock.Mock
structure, which provides all functionality required by mocking. Aside wrapping around mock.Mock
, the struct may declare other fields, as needed. SomethingMock
should implement all the methods that are going to be used in testing. Implementation examples are provided below: the method implementation should forward the invocation to the internal mock instance with Called(args)
and return what the mock returns as result of Called()
. If we know for sure that a method will not be exercised in testing, it is fine to let it panic("not yet implemented")
.
package somepkg
import "github.com/stretchr/testify/mock"
type SomethingMock struct {
mock.Mock
// other fields may be added here
}
// Something interface implementation
func (s *SomethingMock) SomeFunc(sa string, i int) (string, error) {
r := s.Called(sa, i)
return r.String(0), r.Error(1)
}
func (s *SomethingMock) SomeOtherFunc(sa string) (string, error) {
panic("not yet implemented")
}
Instantiate the Mock
Instantiate the mock in the testing code with new()
or &SomethingMock{}
:
func TestSomething(t *testing.T) {
mock := new(SomethingMock)
// or mock := &SomethingMock{}
[...]
}
Set Up Expectations
After instantiation, we configure the mock's behavior by configuring its methods' responses to invocations. Testify calls this stage "setting up the expectations".
// On(function_name, arguments ...).Return(return_value_1, return_value_2, ...)
mock.On("SomeFunc", "fish", 3).Return("bouillabaisse ", nil)
When the function is invoked with arguments different than the ones it was configured with, the mock with panic with:
mock: Unexpected Method Call
We can use specific argument values when configuring the method behavior, like in the example above, or we can use placeholders (mock.Anything
) for each argument where the data being passed in is dynamically generated and cannot be predicted beforehand:
import (
"testing"
testifymock "github.com/stretchr/testify/mock"
)
[...]
mock.On("SomeFunc", testifymock.Anything, testifymock.Anything).Return("bouillabaisse ", nil)
It is important to use the same number of arguments as in the method signature when configuring the mock withOn()
andAnything
Test with the Mock and Verify the Results
Pass the mock to the code that needs to be tested, run the code and ensure it behaves correctly, knowing that the mock returnes what we instructed it to return.
// this is the code to be tested
func Usage(si Something, s string, i int) (string, error) {
return si.SomeFunc(s, i)
}
This is the test:
func TestUsage(t *testing.T) {
assert := tassert.New(t)
mock := new(SomethingMock)
mock.On("SomeFunc", "fish", 3).Return("bouillabaisse", nil)
sr, err := Usage(mock, "fish", 3)
assert.Equal("bouillabaisse", sr)
assert.Nil(err)
}
Assert Expectations
func TestUsage(t *testing.T) {
[...]
mock.AssertExpectations(t)
}
Reconfigure
The mock instances can be "unconfigured" with Unset()
invoked on the result of On()
and then reconfigured again with On
:
func TestUsage(t *testing.T) {
mockCall := mock.On("SomeFunc", ....)
// test
[...]
mock.AssertExpectations(t)
// unset
mockCall.Unset()
// reconfigure
mockCall := mock.On("SomeFunc", ....)
// test
[...]
}