A Go mocking framework.
Moka is a mocking framework for the Go programming
language. Moka works very well with the Ginkgo testing
framework, but can be easily used with any other
testing framework, including the testing
package from the standard library.
go get github.com/gcapizzi/moka
Moka is designed to play well with Ginkgo. All you'll need to do is:
moka
package;Fail
function as Moka's double fail handler using
RegisterDoublesFailHandler
.Here's an example:
package game_test
import (
. "github.com/gcapizzi/moka"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestGame(t *testing.T) {
RegisterFailHandler(Fail)
RegisterDoublesFailHandler(Fail)
RunSpecs(t, "Game Suite")
}
testing
and other frameworksSupport for the testing
package hasn't been
added yet, but this doesn't mean you can't use Moka with it.
Here is the type for the Moka doubles fail handler:
type FailHandler func(message string, callerSkip ...int)
This type is modelled to match Ginkgo's Fail
function. To use Moka with the
testing
package, just provide a doubles fail handler that makes the test
fail!
Here's an example:
package game
import (
. "github.com/gcapizzi/moka"
"testing"
)
func TestGame(t *testing.T) {
RegisterDoublesFailHandler(func(message string, callerSkip ...int) {
t.Fatal(message)
})
// use Moka here
}
A test double is an object that stands in for another object in your system during tests. The first step to use Moka is to declare a double type. The type will have to:
moka.Double
type;moka.Double
instance, using the Call
method.Let's build a double type for a Die
interface:
package dice
import (
. "github.com/gcapizzi/moka"
)
type Die interface {
Roll(times int) []int
}
type DieDouble struct {
Double
}
func NewDieDouble() DieDouble {
return DieDouble{Double: NewStrictDouble()}
}
func (d DieDouble) Roll(times int) []int {
returnValues, _ := d.Call("Roll", times)
returnedRolls, _ := returnValues[0].([]int)
return returnedRolls
}
Some notes:
Double
instance we are embedding is of type StrictDouble
: strict
doubles will fail the test if they receive a call on a method that wasn't
previously allowed or expected.Call
invocation fails, it will both return an error as its second
return value, and invoke the fail handler. Since we know the fail handler
will immediately stop the execution of the test, we don't need to check for
the error.nil
return values.Now that our double type is ready, let's use it in our tests! We will test a
Game
type, which looks like this:
package game
type Game struct {
die Die
}
func NewGame(die Die) Game {
return Game{die: die}
}
func (g Game) Score() int {
rolls := g.die.Roll(3)
return rolls[0] + rolls[1] + rolls[2]
}
Here is the test:
package game_test
import (
. "github.com/gcapizzi/moka/examples/game"
. "github.com/gcapizzi/moka"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Game", func() {
var die DieDouble
var game Game
BeforeEach(func() {
die = NewDieDouble()
game = NewGame(die)
})
Describe("Score", func() {
It("returns the sum of three die rolls", func() {
AllowDouble(die).To(ReceiveCallTo("Roll").With(3).AndReturn([]int{1, 2, 3}))
Expect(game.Score()).To(Equal(6))
})
})
})
You might be wondering: what happens if I allow a method call that would be
impossible to perform, given the type of my double? Let's say we forgot how the
Die
interface was defined, and wrote a test like this:
Describe("Score", func() {
It("returns the sum of three die rolls", func() {
AllowDouble(die).To(ReceiveCallTo("Cast").With(3).AndReturn([]int{1, 2, 3}))
Expect(game.Score()).To(Equal(9))
})
})
The Die
interface has no method called Cast
, so our configured interaction
will never happen!
To avoid this kind of problems, Moka provides typed doubles, which are associated with a type and will make sure that any configured interaction actually matches the type.
To instantiate a typed double, use the NewStrictDoubleWithTypeOf
constructor:
func NewDieDouble() DieDouble {
return DieDouble{Double: NewStrictDoubleWithTypeOf(DieDouble{})}
}
If run against a typed double, the previous test would fail with a message like this:
Invalid interaction: type 'DieDouble' has no method 'Cast'
Sometimes allowing a method call is not enough. Some methods have side effects, and we need to make sure they have been invoked in order to be confident that our code is working.
For example, let's assume we added a Logger
collaborator to our Game
type:
package game
import "fmt"
type Logger interface {
Log(message string)
}
type StdoutLogger struct{}
func (l StdoutLogger) Log(message string) {
fmt.Println(message)
}
We'll start, as usual, by building our double type:
type LoggerDouble struct {
Double
}
func NewLoggerDouble() LoggerDouble {
return LoggerDouble{Double: NewStrictDoubleWithTypeOf(LoggerDouble{})}
}
func (d LoggerDouble) Log(message string) {
d.Call("Log", message)
}
We can now add the Logger
collaborator to Game
, and test their interaction:
Describe("Score", func() {
It("returns the sum of three die rolls", func() {
AllowDouble(die).To(ReceiveCallTo("Roll").With(3).AndReturn([]int{1, 2, 3}))
ExpectDouble(logger).To(ReceiveCallTo("Log").With("[1, 2, 3]"))
Expect(game.Score()).To(Equal(6))
VerifyCalls(logger)
})
})
We use ExpectDouble
to expect method calls on a double, and VerifyCalls
to verify that the calls have actually been made.
If you need to specify a custom behaviour for your double interactions, of need
to perform assertions on the arguments, you can specify a body for the
interaction using AndDo
:
Describe("Score", func() {
It("returns the sum of three die rolls", func() {
AllowDouble(die).To(ReceiveCallTo("Roll").AndDo(func(times int) []int {
Expect(times).To(BeNumerically(">", 0))
rolls := []int{}
for i := 0; i < times; i++ {
rolls = append(rolls, i+1)
}
return rolls
}))
Expect(game.Score()).To(Equal(6))
})
})
There are a lot of mocking libraries for Go out there, so why build a new one? Compared to those libraries, Moka offers:
On the other hand, Moka currently lacks:
Moka treats variadic arguments are regular slice arguments. This means that:
SomeFunction(args...)
) the slice containing
the variadic values when passing it to Call
.For example, given a Calculator
interface:
type Calculator interface {
Add(numbers ...int) int
}
This is how you should delegate Add
to Call
in the double implementation:
func (d CalculatorDouble) Add(numbers ...int) int {
returnValues, _ := d.Call("Add", numbers)
// ...
}
And this is how you should allow a call to Add
:
AllowDouble(calculator).To(ReceiveCallTo("Add").With([]int{1, 2, 3}).AndReturn(6))