mm

package module
v0.0.0-...-b76add8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2016 License: Apache-2.0 Imports: 7 Imported by: 0

README

methodman

methodman is a stub-based Denpendency Injection tool in Go.

Supported Features

  • Stub-based dependency injection for test. It can hijack any method stub to produce fake response for test scenarios.
  • Optimised to minimise code overhead. So that the api of pkgs remain clean and won't be polluted for test requirement.
  • Supports parallel unittest. Mocking in one goroutine is invisible / isolated from any other goroutine.
  • Supports stub-injection with a temporary func, which could be useful for simulating timeout, panic, mock with internal state (via closure), or any other kind of side-effects.
  • Certain helper support to make conversion from integration-test to unit-test easier. See the sections below.

How to use?

Assuming in my_pkg, you have a method MyFunc that depends on MethodA in another pkg dep_pkg, like this,

package my_pkg

...

var MyFunc = func() (resp1, resp2 string){
	resp1, resp2 = dep_pkg.MethodA()
	return 
}

Now I'd like to write some unittest for MyFunc with mocking its dependency dep_pkg.MethodA.

1. Make sure methodman is installed.
go get -u github.com/jason-xxl/methodman
2. Register the method to be mocked (dep_pkg.MethodA) with a valid name.
func TestMain(m *testing.M) {
	flag.Parse()
	mm.EnableMock(&dep_pkg.MethodA, "dep_pkg.MethodA")
	os.Exit(m.Run())
}
3. Mock it in your test.
func TestNormalUse(t *testing.T) {

	defer mm.Init(t).CleanUp()
	
	mm.Expect(&dep_pkg.MethodA, "some fake response as 1st returned var", "some more, as 2nd retuened var")

	// Then you can receive above 2 value in your code path.
	// Inside MyFunc, "dep_pkg.MethodA" would be called, but since it's mocked, you will receive the fake response,
	// instead of executing the real logic of dep_pkg.MethodA.
	ret1, ret2 := MyFunc()
	
	if ret1 == "some fake response as 1st returned var" && ret2 == "some more, as 2nd retuened var" {
		t.Log("awesome! I received the fake responses!")
	}
	
	// 1. If all fake responses are consumed, the agent will fall back to original method.
	// 2. It doesn't matter next call of dep_pkg.MethodA is at which level, above fake value would be 
	//    received if it's in same goroutine.
}

How methodman works?

The stub based approach is simple.

  1. When registering the dependency method var, methodman will replace (monkey patch) the var with a manager object. It wraps the original method with a queue layer in front (one queue for one method in one goroutine).

  2. When you push a fake response to a method, the response will enter the queue of current goroutine.

  3. When the method endpoint is called (actually the manager object is called), it will check the queue of current goroutine. If the queue is non-empty, the method will response the fake response by consuming the queue. When queue is empty, the original method is called to provide a real response.

How to enable monkey patching (stub injection) in Golang?

In many dynamic languages it's easy to monkey patching object or methods. However, in Go, exported pkg method is not modifiable. So by default there's no formal way to monkey-patch. To get monkey patching works, pkg method have to be defined as an exported method variable to allow modify (in case you can control the code),

  var TheMethodToBeMocked = func(...){...}

Or, use a reference var in caller side like this (in case you can't control the code),

  var TheMethodToBeMocked = targetpkg.TargetMethod

Then you can monkey patch TheMethodToBeMocked for mocking.

Given the target method var is modifiable, methodman would replace TheMethodToBeMocked with a wrapper, who provides mockability that just overlays the original method.

How to convert integration-test into unittest? (For refactoring scenarios)

Assuming you have a test case that accesses external dependencies and already works fine, assuming no forking gorouting inside the logic, and now, you want to convert it into a unittest.

So the idea here is you run your integration-test once, capture those real outputs of depending methods, and use those output as sample to mock. Here's step by step.

1. Enable the CapturingLogger and register the methods that you want to mock.
func TestMain(m *testing.M) {
	flag.Parse()

	mm.SetLogger(mm.CapturingLogger)

	mm.EnableMock(&dep_pkg.MethodA, "dep_pkg1.MethodA")
	mm.EnableMock(&dep_pkg.MethodB, "dep_pkg2.MethodA")

	os.Exit(m.Run())
}
2. Run your test in verbose mode, you can get the real response in the form of usable code that you can insert into your code. This save human effort to form the mock response and reduce human error.
go test -v -run TestToBeConverted

In test output, you gain the output from original method in the way copy-pastable.

...
mm.Expect(&dep_pkg1.MethodA, "real response 1", "real response 2")
mm.Expect(&dep_pkg2.MethodB, "real response 3")
...
...
3. By copying them into your test, you gain the unittest version based on previous integration.
func TestToBeConverted(t *testing.T) {

    defer mm.Init(t).CleanUp()

    mm.Expect(&dep_pkg1.MethodA, "real response 1", "real response 2")
    mm.Expect(&dep_pkg2.MethodB, "real response 3")

    ... // Your original code here. No change.
}
4. After that, remove this line.
mm.SetLogger(mm.CapturingLogger)

Now you got a perfect unittest that fully detached from real backends.

How can I fake an object returned by the dependency pkg? (For refactoring scenarios)

Methodman is modeled around modifiable method stub, so it won't natively work for this kind of faking object.

When converting your implementation to allow Dependency Injection for this case, you probably will,

  1. Abstractise the returned type into an interface, which allow using a mock implementation behind

  2. Probably you'll use codegen tool like Testify Mock Mokery to generate the mock implementation in independent files

  3. By normal practice, you will need to change your main logic to receive dependency as extra param, to allow mocking in unittest.

However, for Step 3, Methodman makes it easier. You can just patch the function where you receive the object, either object constructor, or a singelton getter. Simply hijack it to response with your mock object, and that's all. No need to refactor main logic for enable mocking.

What about mocking an exported channel without significant refactoring? I'm not sure yet. Please share with me if you got idea.

Complete Demo

Please check out GitHub Pages

Documentation

Overview

Package mm tries to minimise code change to support Dependency Injection to support mocking in unittest. The assumption it makes is very minimal, just make sure the exported methods from your lib are in a form of exported variables. So it provides a manager for your method behind the scene, if you enqueued a fake response for you test in current goroutine, calling the (managed) method will firstly response your fake response instead to calling the original. Note that, for putting minimal code footprint as one of top priority, this lib will use panic for failed assertion check (like parameter check) instead of passing err back to user that simply make final code bloated.

Index

Constants

This section is empty.

Variables

View Source
var (

	// DefaultLogger is for minimal and human readable logging
	DefaultLogger = func(methodName string, isMockResponse bool, output []interface{}) {
		loggerMutex.Lock()
		defer loggerMutex.Unlock()

		var tag string
		if isMockResponse {
			tag = "[mock]"
		} else {
			tag = "[real]"
		}

		tpl := tag + " %s"

		t := ", %#v"
		t = strings.Repeat(t, len(output))

		tpl += t
		tpl += ""

		output = append([]interface{}{methodName}, output...)

		code := pretty.Sprintf(tpl, output...)

		fmt.Println(code)
	}

	// CapturingLogger organise the real output in code form that you can copy paste.
	// It's a handy helper for converting integration test into unittest.
	// 1. set it as logger
	// 2. call "go test -v -run yourtest"
	CapturingLogger = func(methodName string, isMockResponse bool, output []interface{}) {
		if !isMockResponse {
			return
		}

		loggerMutex.Lock()
		defer loggerMutex.Unlock()

		tpl := "mm.Expect(&%s"

		t := ", %#v"
		t = strings.Repeat(t, len(output))

		tpl += t
		tpl += ")"

		output = append([]interface{}{methodName}, output...)

		code := pretty.Sprintf(tpl, output...)

		fmt.Println(code)
	}
)

Functions

func EnableMock

func EnableMock(method interface{}, name string)

EnableMock upgrades a method to allow per-goroutine mocking. Note that, 1) it will replace your method with method helper, so do it at beginning of all your unittests, not in the middle, to avoid race condition. 2) if default queue length (200) is not enough for you, you can enlarge by giving the queueLength. It panic if you gave invalid value. 3) it's expected to be called at ONLY init phase of tests, so no need to protect with Mutex

func Expect

func Expect(method interface{}, response ...interface{})

Expect adds a mocking response to a queue of given method. Note that, 1) the queue is tied to current goroutine, so call from different goroutine won't see this result. 2) for the expected response, first in first out 3) if no more expected response in queue, the original function would be called to serve the call 4) you can enqueue mock objects to its constructing function 5) it panic if you gave invalid value

func ExpectFunc

func ExpectFunc(method interface{}, fakeFunc interface{})

ExpectFunc adds a temp implementation of the original method and consume once It would be helpful for some special case like simulating a timeout.

func GetCurrentT

func GetCurrentT() (t *testing.T)

GetCurrentT gets current T object from gls

func GetMethodType

func GetMethodType(method interface{}) (typeMethod reflect.Type)

GetMethodType ...

func IsMethod

func IsMethod(method interface{}) (yes bool)

IsMethod ...

func IsMethodPointer

func IsMethodPointer(methodPointer interface{}) (yes bool)

IsMethodPointer ...

func ResetQueue

func ResetQueue(method interface{})

ResetQueue flushed resp queue for a method under current goroutine

func SetLogger

func SetLogger(logger Logger)

SetLogger Logger would be used during the test, default is disabled, you should set it up (only once) before test runs if you need it

func SetQueueLength

func SetQueueLength(newLength int)

SetQueueLength ...

func ValueSliceToInterfaceSlice

func ValueSliceToInterfaceSlice(valueSlice []reflect.Value) (interfaceSlice []interface{})

ValueSliceToInterfaceSlice convert []reflect.Value to []interface{}

Types

type Initiator

type Initiator struct {
	// CleanUp will do clean up gls state for current goroutine
	CleanUp func()
}

func Init

func Init(t *testing.T) *Initiator

Init store the current *testing.T for use By default you can use it at beginning of your test like this way

defer mm.Init(t).CleanUp()

type Logger

type Logger func(methodName string, isMockResponse bool, output []interface{})

Logger should be an external method for logging the captured content by default the logger is empty and capturing is disabled note: Assuming it's set once at testing beginning, it's not Mutex protected. Don't change it during test.

type Manager

type Manager struct {
	Name   string
	Method *OriginalMethod
}

Manager maintains states of a method wrapper, which will take over control of your original method. The basic idea is, for each calls it will 1) check if any expected fake response in the queue for current goroutine 2) if yes, consume the response, otherwise call original method like nothing happen

func ManagerNew

func ManagerNew(name string, method interface{}) (o *Manager)

ManagerNew ...

type ManagerMap

type ManagerMap map[MethodUniqueID]*Manager

ManagerMap stores mapping relation from pointer of manager method to its state struct. Note that, 1) the pointer of original method is in the manager state. 2) assuming the map is formed from beginning of test and remain no changed, so no need to add sync.RWMutex protection.

func ManagerMapNew

func ManagerMapNew() (o ManagerMap)

ManagerMapNew ...

type MethodUniqueID

type MethodUniqueID string

MethodUniqueID is string representation of a method's identifier (memory address) in current running instance. It's not guaranteed to be the same after process restarts. For being easy to understand it use fmt pkg and a string form instead of unsafe.Pointer

func GetMethodUniqueID

func GetMethodUniqueID(method interface{}) (id MethodUniqueID)

GetMethodUniqueID ...

type OriginalMethod

type OriginalMethod struct {
	// contains filtered or unexported fields
}

OriginalMethod ...

func OriginalMethodNew

func OriginalMethodNew(methodName string, method interface{}) (o *OriginalMethod, ok bool)

OriginalMethodNew ...

func (*OriginalMethod) Apply

func (o *OriginalMethod) Apply(input []reflect.Value) (output []reflect.Value)

Apply ...

func (*OriginalMethod) MakeFunc

func (o *OriginalMethod) MakeFunc() (f reflect.Value)

MakeFunc ...

type RespQueue

type RespQueue chan []interface{}

RespQueue is a blocking queue storing expected / fake response for use

func GetLocalRespQueue

func GetLocalRespQueue(fullKey string) (o RespQueue)

GetLocalRespQueue ...

func RespQueueNew

func RespQueueNew(length int) (o RespQueue)

RespQueueNew ...

func (RespQueue) Flush

func (o RespQueue) Flush()

Flush ...

func (RespQueue) Push

func (o RespQueue) Push(element []interface{}) (ok bool)

Push ...

func (RespQueue) Shift

func (o RespQueue) Shift() (element []interface{}, ok bool)

Shift ...

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL