Go: Interface Mocking With Mockc
Introduction
There are so many mocking libraries in Go ecosystem. But most of them are very difficult to use and learn. I think the main reason is they force you to use specific functions for assertion. Even if those functions provide powerful features, they are usually not intuitive and type-safe.
So, I created a simple mock generator so that you could test your code in an intuitive way. And its name is 'Mockc'.
Let's take an overview!
Overview
It provides two ways for generating mock. In this post, I'll only cover the way using Mock Generator.
Before we get started, install the command-line tool mockc
first!
go get github.com/KimMachineGun/mockc/cmd/mockc
Then you'll get mockc
installed in your $GOPATH/bin
.
This is our target interface and simple implementation of it.
// cache.go
package main
type Cache interface {
Get(key string) (val interface{}, err error)
Set(key string, val interface{}) (err error)
Del(key string) (err error)
}
type MapCache struct {
m map[string]interface{}
}
func (c MapCache) Get(key string) (interface{}, error) {
if c.m != nil {
c.m = map[string]interface{}{}
}
return c.m[key], nil
}
func (c MapCache) Set(key string, val interface{}) error {
if c.m != nil {
c.m = map[string]interface{}{}
}
c.m[key] = val
return nil
}
func (c MapCache) Del(key string) error {
if c.m != nil {
c.m = map[string]interface{}{}
}
delete(c.m, key)
return nil
}
func HasKey(c Cache, key string) (bool, error) {
val, err := c.Get(key)
if err != nil {
return false, err
}
return val != nil, nil
}
Before generating mock, you need to know about the concept of Mock Generator.
Mock Generator: A configuration code for generating mock written in Go syntax. It allows you to configure the name of mock, destination file, constructor generating option, etc.
Let's write a Mock Generator.
// mockc.go
//+build mockc
package main
import (
"github.com/KimMachineGun/mockc"
)
func MockcCache() {
mockc.Implement(Cache(nil))
mockc.WithConstructor()
}
Note: In order to ignore Mock Generator file in regular compilation, you should put the build tag //+build mockc
at the first line of your file.
The func MockcCache()
is the Mock Generator. Mock Generator must be a function and call mockc.Implement
with the interfaces that you want to implement. And, the name of function(MockcCache
) will be the name of mock.
In this example, MockcCache
calls mockc.WithConstructor
too. It is an optional configuration function, and it means the mock constructor will be generated together.
You can find all configuration functions such as mockc.WithConstructor
here.
Now, you can generate mock by using the command-line tool mockc we installed before.
mockc
After you run this command, mockc_gen.go
will be generated.
// mockc_gen.go
// Code generated by Mockc. DO NOT EDIT.
// repo: https://github.com/KimMachineGun/mockc
//go:generate mockc
// +build !mockc
package main
import (
"sync"
)
type MockcCache struct {
// method: Del
_Del struct {
mu sync.Mutex
// basics
Called bool
CallCount int
// call history
History []struct {
Params struct {
P0 string
}
Results struct {
R0 error
}
}
// params
Params struct {
P0 string
}
// results
Results struct {
R0 error
}
// if it is not nil, it'll be called in the middle of the method.
Body func(string) error
}
// method: Get
_Get struct {
mu sync.Mutex
// basics
Called bool
CallCount int
// call history
History []struct {
Params struct {
P0 string
}
Results struct {
R0 interface{}
R1 error
}
}
// params
Params struct {
P0 string
}
// results
Results struct {
R0 interface{}
R1 error
}
// if it is not nil, it'll be called in the middle of the method.
Body func(string) (interface{}, error)
}
// method: Set
_Set struct {
mu sync.Mutex
// basics
Called bool
CallCount int
// call history
History []struct {
Params struct {
P0 string
P1 interface{}
}
Results struct {
R0 error
}
}
// params
Params struct {
P0 string
P1 interface{}
}
// results
Results struct {
R0 error
}
// if it is not nil, it'll be called in the middle of the method.
Body func(string, interface{}) error
}
}
func NewMockcCache(
v ...interface {
Del(p0 string) error
Get(p0 string) (interface{}, error)
Set(p0 string, p1 interface{}) error
},
) *MockcCache {
m := &MockcCache{}
if len(v) > 0 {
m._Del.Body = v[0].Del
m._Get.Body = v[0].Get
m._Set.Body = v[0].Set
}
return m
}
func (recv *MockcCache) Del(p0 string) error {
recv._Del.mu.Lock()
defer recv._Del.mu.Unlock()
// basics
recv._Del.Called = true
recv._Del.CallCount++
// params
recv._Del.Params.P0 = p0
// body
if recv._Del.Body != nil {
recv._Del.Results.R0 = recv._Del.Body(p0)
}
// call history
recv._Del.History = append(recv._Del.History, struct {
Params struct {
P0 string
}
Results struct {
R0 error
}
}{
Params: recv._Del.Params,
Results: recv._Del.Results,
})
// results
return recv._Del.Results.R0
}
func (recv *MockcCache) Get(p0 string) (interface{}, error) {
recv._Get.mu.Lock()
defer recv._Get.mu.Unlock()
// basics
recv._Get.Called = true
recv._Get.CallCount++
// params
recv._Get.Params.P0 = p0
// body
if recv._Get.Body != nil {
recv._Get.Results.R0, recv._Get.Results.R1 = recv._Get.Body(p0)
}
// call history
recv._Get.History = append(recv._Get.History, struct {
Params struct {
P0 string
}
Results struct {
R0 interface{}
R1 error
}
}{
Params: recv._Get.Params,
Results: recv._Get.Results,
})
// results
return recv._Get.Results.R0, recv._Get.Results.R1
}
func (recv *MockcCache) Set(p0 string, p1 interface{}) error {
recv._Set.mu.Lock()
defer recv._Set.mu.Unlock()
// basics
recv._Set.Called = true
recv._Set.CallCount++
// params
recv._Set.Params.P0 = p0
recv._Set.Params.P1 = p1
// body
if recv._Set.Body != nil {
recv._Set.Results.R0 = recv._Set.Body(p0, p1)
}
// call history
recv._Set.History = append(recv._Set.History, struct {
Params struct {
P0 string
P1 interface{}
}
Results struct {
R0 error
}
}{
Params: recv._Set.Params,
Results: recv._Set.Results,
})
// results
return recv._Set.Results.R0
}
The generated MockCache
implements the interfaces you provided before. And you can set the return values of the method and get the call count and call histories through its fields(by default, the field name will be '_{MethodName}
' and you can change the prefix and suffix by using the mockc.SetFieldNamePrefix
and mockc.SetFieldNameSuffix
).
Once generated successfully, you can use it right away!
This is my example test code.
// cache_test.go
package main
import (
"errors"
"testing"
)
func TestHasKey(t *testing.T) {
// m := &MockcCache{}
m := NewMockcCache()
// set return value
m._Get.Results.R0 = struct{}{}
// execute
key := "test_key"
result, err := HasKey(m, key)
// assert
if !result {
t.Error("result should be true")
}
if err != nil {
t.Error("err should be nil")
}
if m._Get.CallCount != 1 {
t.Errorf("Cache.Get should be called once: actual(%d)", m._Get.CallCount)
}
if m._Get.Params.P0 != key {
t.Errorf("Cache.Get should be called with %q: actual(%q)", key, m._Get.Params.P0)
}
}
func TestHasKey_WithBodyInjection(t *testing.T) {
m := NewMockcCache()
// inject body
key := "test_key"
m._Get.Body = func(actualKey string) (interface{}, error) {
if actualKey != key {
t.Errorf("Cache.Get should be called with %q: actual(%q)", key, actualKey)
}
return nil, errors.New("error")
}
// execute
result, err := HasKey(m, key)
// assert
if result {
t.Error("result should be false")
}
if err == nil {
t.Error("err should not be nil")
}
if m._Get.CallCount != 1 {
t.Errorf("Cache.Get should be called once: actual(%d)", m._Get.CallCount)
}
}
func TestHasKey_WithMapCache(t *testing.T) {
// set the underlying implementation by passing real implementation to constructor
m := NewMockcCache(MapCache{})
// execute
key := "key"
result, err := HasKey(m, key)
// assert
if result {
t.Error("result should be false")
}
if err != nil {
t.Error("err should be nil")
}
if m._Get.CallCount != 1 {
t.Errorf("Cache.Get should be called once: actual(%d)", m._Get.CallCount)
}
if m._Get.History[0].Params.P0 != key {
t.Errorf("Cache.Get should be called with %q: actual(%q)", key, m._Get.History[0].Params.P0)
}
}
You can find more examples here.
Conclusion
In this post, we took an overview of Mockc. I agree that it's not a perfect tool. There must be pros and cons. Some people may not like its approach(using build tag, code generation, etc...). Still, some will like its straightforwardness and simplicity.
Mockc is evolving rapidly, and it really needs your feedback and contribution. Any kind of feedback is much appreciated.
Thank you!
Comments
Post a Comment