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

Popular Posts

Go: Mockc로 인터페이스 모킹하기