Kelpie is the most magical mock generator for Go. Kelpie aims to be easy to use, and generates fully type-safe mocks for Go interfaces.
At the moment Kelpie is very much in development, and there are missing features and some pretty rough edges. You're of course welcome to use Kelpie, but just be prepared to hit problems and raise issues or PRs!
The following is a list of known-outstanding features and known issues:
- Add support for embedded interfaces.
Install Kelpie:
go install github.com/adamconnelly/kelpie/cmd/kelpie@latest
Add a go:generate
marker to the interface you want to mock:
//go:generate kelpie generate --package github.com/someorg/some/package --interfaces EmailService
type EmailService interface {
Send(sender, recipient, body string) (cost float64, err error)
}
Use the mock:
emailServiceMock := emailservice.NewMock()
emailServiceMock.Setup(
emailservice.Send("[email protected]", "[email protected]", kelpie.Any[string]()).
Return(100.54, nil)
)
emailServiceMock.Setup(
emailservice.Send("[email protected]", "[email protected]", kelpie.Any[string]()).
Return(0, errors.New("that domain is forbidden!"))
)
service := emailServiceMock.Instance()
service.Send("[email protected]", "[email protected]", "Amazing message")
// Returns 100.54, nil
service.Send("[email protected]", "[email protected]", "Hello")
// Returns 0, errors.New("that domain is forbidden!)
There are two main ways to generate your mocks:
- Using
go:generate
comments. - Using a kelpie.yaml file.
Using go:generate
comments is simple - take a look at the Quickstart for an example.
The other option is to add a kelpie.yaml file to your repo. The advantage of this is that all of your mocks are defined in one place, and mock generation can be significantly quicker than the go:generate
approach because it avoids unnecessary duplicate parsing.
To do this, add a kelpie.yaml file to the root of your repo like this:
version: 1
packages:
# You can mock packages that aren't part of your repo. To do this just specify the package
# name as normal:
- package: io
# When mocking packages outside your source tree, remember to specify the directory the
# mocks should be generated in.
directory: examples/mocks
mocks:
- interface: Reader
- package: github.com/adamconnelly/kelpie/examples
# Mocks defines the interfaces within your package that you want to generate mocks for.
mocks:
- interface: Maths
- interface: RegistrationService
generation:
# Package sets the package name generated for the mock. By default the package name
# is the lower-cased interface name.
package: regservice
To generate the mocks, just run kelpie generate
:
$ kelpie generate
Kelpie mock generation starting - preparing to add some magic to your code-base!
Parsing package 'io' for interfaces to mock.
- Generating a mock for 'Reader'.
Parsing package 'github.com/adamconnelly/kelpie/examples' for interfaces to mock.
- Generating a mock for 'Maths'.
- Generating a mock for 'RegistrationService'.
Mock generation complete!
No setup, no big deal. Kelpie returns the default values for method calls instead of panicking:
mock := emailservice.NewMock()
mock.Instance().Send("[email protected]", "[email protected]", "Hello world")
// Returns 0, nil
Kelpie always uses the most recent expectation when trying to match a method call. That way you can easily override behaviour. This is really useful if you want to for example specify a default behaviour, and then later test an error condition:
mock := emailservice.NewMock()
// Setup an initial behaviour
mock.Setup(
emailservice.Send(kelpie.Any[string](), kelpie.Any[string](), kelpie.Any[string]()).
Return(200, nil)
)
service := mock.Instance()
cost, err := service.Send("[email protected]", "[email protected]", "Hello world")
t.Equal(200, cost)
t.NoError(err)
// We override the mock, to allow us to test an error condition
mock.Setup(
emailservice.Send(kelpie.Any[string](), kelpie.Any[string](), kelpie.Any[string]()).
Return(0, errors.New("no way!"))
)
cost, err := service.Send("[email protected]", "[email protected]", "Hello world")
t.Equal(0, cost)
t.ErrorEqual(err, "no way!")
By default Kelpie uses exact matching, and any parameters in a method call need to exactly match those specified in the setup:
emailServiceMock.Setup(
emailservice.Send("[email protected]", "[email protected]", "Hello world").
Return(100.54, nil)
)
You can match against any possible values of a particular parameter using kelpie.Any[T]()
:
emailServiceMock.Setup(
emailservice.Send(kelpie.Any[string](), "[email protected]", "Hello world").
Return(100.54, nil)
)
You can add custom argument matching functionality using kelpie.Match[T](isMatch)
:
emailServiceMock.Setup(
emailservice.Send(
kelpie.Match(func(sender string) bool {
return strings.HasSuffix(sender, "@discounted-sender.com")
}),
"[email protected]",
"Hello world!").
Return(50, nil))
To return a specific value from a method call, use Return()
:
emailServiceMock.Setup(
emailservice.Send("[email protected]", "[email protected]", "Hello world").
Return(100.54, nil)
)
To panic, use Panic()
:
emailServiceMock.Setup(
emailservice.Send("[email protected]", kelpie.Any[string](), "testing").
Panic("Something has gone badly wrong!")
)
To perform a custom action, use When()
:
emailServiceMock.Setup(
emailservice.Send(kelpie.Any[string](), kelpie.Any[string](), kelpie.Any[string]()).
When(func(sender, recipient, body string) (float64, error) {
// Do something
return 0, nil
}))
You can verify that a method has been called using the mock.Called()
method:
// Arrange
mock := registrationservice.NewMock()
// Act
mock.Instance().Register("Mark")
mock.Instance().Register("Jim")
// Assert
t.True(mock.Called(registrationservice.Register("Mark")))
t.True(mock.Called(registrationservice.Register(kelpie.Any[string]()).Times(2)))
t.False(mock.Called(registrationservice.Register("Wendy")))
You can configure a method call to only match a certain number of times, or verify a method has been called a certain number of times using the Times()
, Once()
and Never()
helpers:
// Arrange
mock := registrationservice.NewMock()
// Act
mock.Instance().Register("Mark")
mock.Instance().Register("Jim")
// Assert
t.True(mock.Called(registrationservice.Register("Mark").Once()))
t.True(mock.Called(registrationservice.Register(kelpie.Any[string]()).Times(2)))
t.True(mock.Called(registrationservice.Register("Wendy").Never()))
You can mock methods that accept variable parameter lists, but there are some caveats to be aware of. Here's a simple example using exact matching:
type Printer interface {
Printf(formatString string, args ...interface{}) string
}
func (t *VariadicFunctionsTests) Test_Parameters_ExactMatch() {
// Arrange
mock := printer.NewMock()
mock.Setup(printer.Printf("Hello %s. This is %s, %s.", "Dolly", "Louis", "Dolly").Return("Hello Dolly. This is Louis, Dolly."))
// Act
result := mock.Instance().Printf("Hello %s. This is %s, %s.", "Dolly", "Louis", "Dolly")
// Assert
t.Equal("Hello Dolly. This is Louis, Dolly.", result)
}
Because of the way generics work, you can't mix exact matching with custom matching. So for example the following will work:
mock.Setup(printer.Printf("Hello %s. This is %s, %s.", kelpie.ExactMatch("Dolly"), kelpie.Any[string](), kelpie.ExactMatch("Dolly")).
Return("Hello Dolly. This is Louis, Dolly."))
But the following will not compile:
mock.Setup(printer.Printf("Hello %s. This is %s, %s.", "Dolly", kelpie.Any[string](), "Dolly").
Return("Hello Dolly. This is Louis, Dolly."))
If your variadic parameter is ...any
or ...interface{}
, and you try to pass in multiple different types of argument, the Go compiler can't infer the types for you. Here's an example:
// Fails with a "mismatched types untyped string and untyped int (cannot infer P1)" error
mock.Called(printer.Printf("Hello world!", "One", 2, 3.0))
To fix this, just specify the type parameters:
mock.Called(printer.Printf[string, any]("Hello world!", "One", 2, 3.0))
If you want to match that a variadic function call is made with no arguments provided, you can use kelpie.None[T]()
:
mock.Setup(printer.Printf("Hello world", kelpie.None[any]()))
mock.Called(secrets.Get(kelpie.Any[context.Context](), kelpie.Any[string](), kelpie.None[any]()))
The reason for using None
is that otherwise the Go compiler can't infer the type of the variadic parameter:
// Fails with "cannot infer P1"
mock.Setup(printer.Printf("Nothing to say").Return("Nothing to say"))
Another option instead of using None
is to specify the type arguments explicitly, but that can become very verbose, especially when using Kelpie's matching functions:
secretsManagerMock.Called(
secretsmanagerapi.PutSecretValue[mocking.Matcher[context.Context], mocking.Matcher[*secretsmanager.PutSecretValueInput], func(*secretsmanager.Options)](
kelpie.Any[context.Context](), kelpie.Any[*secretsmanager.PutSecretValueInput]()))
Similar to the way that you can match against no parameters with kelpie.None[T]()
, you can match that any amount of parameters are passed to a variadic function using kelpie.AnyArgs[T]()
:
mock.Setup(printer.Printf("Don't panic!", kelpie.AnyArgs[any]()).Panic("Ok!"))
Kelpie supports mocking interfaces defined as struct fields. This can be useful in situations where you want to define an interface to decouple and make testing easier, but that interface isn't used anywhere else.
To generate a mock for a nested interface, just use the format <Struct Name>.<Field Name>
to reference the nested interface, like in the following example:
//go:generate kelpie generate --package github.com/adamconnelly/kelpie/examples --interfaces ConfigService.Encrypter
//go:generate kelpie generate --package github.com/adamconnelly/kelpie/examples --interfaces ConfigService.Storage
type ConfigService struct {
Encrypter interface {
Encrypt(value string) (string, error)
}
Storage interface {
StoreConfigValue(key, value string) error
}
}
func (c *ConfigService) StoreConfig(key, value string) error {
encryptedValue, err := c.Encrypter.Encrypt(value)
if err != nil {
return err
}
return c.Storage.StoreConfigValue(key, encryptedValue)
}
We can then use the mocks like this:
func (t *NestedInterfacesTests) Test_ConfigService_StoresEncryptedValue() {
// Arrange
encrypterMock := encrypter.NewMock()
storageMock := storage.NewMock()
encrypterMock.Setup(encrypter.Encrypt("unencrypted").Return("encrypted", nil))
configService := &ConfigService{
Encrypter: encrypterMock.Instance(),
Storage: storageMock.Instance(),
}
// Act
err := configService.StoreConfig("kelpie.testSecret", "unencrypted")
// Assert
t.NoError(err)
t.True(storageMock.Called(storage.StoreConfigValue("kelpie.testSecret", "encrypted")))
}
If you need to mock an interface that's nested inside another struct, just specify the dot-separated path to the interface. For example MyStruct.NestedField.InterfaceToMock
.
Under the hood, Kelpie uses Go generics to allow either the actual parameter type or a Kelpie matcher to be passed in when setting up mocks or verifying expectations. For example, say we have the following method:
Add(a, b int) int
Kelpie will generate the following method for configuring expectations on Add
:
func Add[P0 int | mocking.Matcher[int], P1 int | mocking.Matcher[int]](a P0, b P1) *addMethodMatcher {
This is neat, because it allows each parameter to either be an int
, or a Kelpie matcher, allowing you to write simple setups like this:
mock.Setup(maths.Add(10, 20).Return(30))
Unfortunately Go generics don't allow a union that contains a non-empty interface. Because of this if any of your parameters accept an interface, you need to use a Kelpie matcher. For example the following won't work:
var ctx context.Context
mock.Setup(secrets.Get(ctx, "MySecret").Return("SuperSecret"))
But the following will:
var ctx context.Context
mock.Setup(secrets.Get(kelpie.Any[context.Context](), "MySecret").Return("SuperSecret"))
Kelpie can happily mock interfaces that aren't part of your own source. You don't need to do anything special to mock an "external" interface - just specify the package and interface name you want to mock:
//go:generate kelpie generate --package io --interfaces Reader
func (t *ExternalTypesTests) Test_CanMockAnExternalType() {
// Arrange
var bytesRead []byte
mock := reader.NewMock()
mock.Setup(reader.Read(kelpie.Match(func(b []byte) bool {
bytesRead = b
return true
})).Return(20, nil))
// Act
read, err := mock.Instance().Read([]byte("Hello World!"))
// Assert
t.NoError(err)
t.Equal(20, read)
t.Equal([]byte("Hello World!"), bytesRead)
}
Kelpies are magical creatures from Scottish folk-lore that have shape-shifting abilities. This name seemed fitting for a mocking library, where generated mocks match the shape of interfaces that you want to simulate.
But other than that, there's nothing very magical about Kelpie.