Mocking is an important thing for unit testing. There are many mocking frameworks that can help us to write unit tests but they might be too big to install for a small project. There might be only one place to use the framework. It’s not good to install the big module in this case.
Maybe you are in such a situation and thus come to this post. Let’s try to create a mock object by ourselves.
You can see the complete code in my GitHub repository.
Creating an interface to mock
Firstly, let’s check the code that we want to write unit tests.
package selfmock
import "fmt"
type Provider struct {
reader Reader
}
func NewProvider(reader Reader) *Provider{
instance := new(Provider)
instance.reader = reader
return instance
}
func (p *Provider) ProvideData() (string, error) {
err := p.reader.Open()
if err != nil {
return "", fmt.Errorf("failed to open: %w", err)
}
defer p.reader.Close()
data, err := p.reader.Read(99)
if err != nil {
return "", fmt.Errorf("failed to read: %w", err)
}
return data, nil
}
The Provider
has Reader
which is used in the main function ProvideData
. To write tests for the function, we need to control the behavior. It means that the mock object needs to be injected from outside. That’s why NewProvider
accepts Reader
parameter.
The following code is the actual code for the Reader.
package selfmock
import "fmt"
type FileReader struct {
Path string
}
func NewFileReader(path string) *FileReader {
instance := new(FileReader)
instance.Path = path
return instance
}
func (f *FileReader) Open() error {
fmt.Printf("path: %s", f.Path)
return nil
}
func (f *FileReader) Close() {
}
func (f *FileReader) Read(size int) (string, error) {
return "abcde", nil
}
func (f *FileReader) Something() error {
fmt.Println("Do something")
return nil
}
Assume that the code is provided by built-in or 3rd party module. Then, create an interface against the module.
type Reader interface {
Open() error
Close()
Read(size int) (string, error)
Something() error
}
It might have a lot of functions but only a few functions are used in production code. In this case, define the only used functions to make it more maintainable.
Define a mock struct against the interface
Once an interface is defined, we need to define a struct that fulfills the interface.
type FileReaderMock struct {
selfmock.Reader
spy Spy
FakeOpen func() error
FakeClose func()
FakeRead func(size int) (string, error)
}
All the functions defined in Reader
interface can be used in the FileReaderMock
struct in this way without defining them explicitly.
Other variables Fakexxxx
are used to define the behavior depending on a test requirement. For example, we need to make the function call success for a normal case. On the other hand, it needs to return an error for the error case. That control is done via FakeOpen
.
spy
is used to have the number of function calls and the args.
Note that the Reader
is defined in package selfmock
and the test code is in package selfmock_test
.
Define Spy functions
The number of function calls and the arguments are sometimes need to be checked in unit tests. Since we don’t use a library, we need to define them.
I implemented it in the following way.
type Spy struct {
CallCount map[string]int
Args map[string][][]any
}
func (s *Spy) Init() {
s.CallCount = make(map[string]int)
s.Args = make(map[string][][]any)
}
func (s *Spy) Register(funcName string, args ...any) {
val := s.CallCount[funcName]
val++
s.CallCount[funcName] = val
values := s.Args[funcName]
values = append(values, args)
s.Args[funcName] = values
}
We have multiple functions. Therefore, we need to store the info separately by using map. The last three lines can be written on a single line if you want.
s.Args[funcName] = append(s.Args[funcName], args)
Define the mock functions
We defined FileReaderMock
struct. We can access the functions but we still have to define the functions’ behavior.
Define the default behavior in the initializer. The default definition is used when we don’t assign a function to Fakexxxx
variable.
func NewFileReaderMock() *FileReaderMock {
instance := new(FileReaderMock)
instance.spy.Init()
instance.FakeOpen = func() error { return nil }
instance.FakeClose = func() {}
instance.FakeRead = func(size int) (string, error) {
return "", errors.New("define the behavior")
}
return instance
}
Then, define the remaining functions. What they do is basically the same. It just calls Fakexxxx
function in it.
const (
openKey = "Open"
closeKey = "Close"
readKey = "Read"
)
func (f FileReaderMock) Open() error {
f.spy.Register(openKey)
return f.FakeOpen()
}
func (f FileReaderMock) Close() {
f.spy.Register(closeKey)
f.FakeClose()
}
func (f FileReaderMock) Read(size int) (string, error) {
f.spy.Register(readKey, size)
return f.FakeRead(size)
}
Okay. The behavior can be defined in each test in this way.
How to use self mock object
Let’s use self mock object here. The test file looks like the following.
package selfmock_test
import (
"errors"
"play-with-go-lang/test/selfmock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Provider", func() {
var readerMock *FileReaderMock
BeforeEach(func() {
readerMock = NewFileReaderMock()
})
Describe("ProvideData", func() {
// tests are here...
})
})
How to define a fake behavior for a function
It is easy to set a fake behavior. Define a function and assign it to Fakexxxx
.
It("should return data", func() {
instance := selfmock.NewProvider(readerMock)
readerMock.FakeRead = func(size int) (string, error) {
return "inject fake data", nil
}
data, err := instance.ProvideData()
Expect(err).ShouldNot(HaveOccurred())
Expect(data).Should(Equal("inject fake data"))
})
We’ve defined the default behavior that returns an error but this test succeeds because the default behavior is overwritten.
How to check the call count and the arguments
Then, next is to get function call info from spy.
It("should return data (call the function twice)", func() {
instance := selfmock.NewProvider(readerMock)
readerMock.FakeRead = func(size int) (string, error) {
return "inject fake data", nil
}
instance.ProvideData()
instance.ProvideData()
Expect(readerMock.spy.CallCount[readKey]).Should(Equal(2))
Expect(readerMock.spy.Args[readKey][0][0]).Should(Equal(99))
Expect(readerMock.spy.Args[readKey][1][0]).Should(Equal(99))
})
The function name needs to be specified in the brackets. Then, you can access the necessary info. For the args, the same function can be called several times and we need to store them for each call.
To get the args for the second call, index 1 needs to be specified. If you need to get the third parameter of the function for the second call, the representation looks like this.
readerMock.spy.Args[readKey][1][2]
Comments