Go Development
Discover how to run successful Golang testing and mocking with Gomock in table-driven tests. Learn more in the Gomock tutorial from GogoApps.
In this tutorial, we will present one the key way our Golang development team tackles the problem of writing precise, robust and explicit table-driven unit tests with the gomock package.
If you’re not familiar with the terms used above, we would recommend referring to the following texts first:
Continue reading this Gomock tutorial to discover the best practices when it comes to running a successful table-driven test with Gomock.
The best way of learning is by getting your hands dirty. To kick things off, let's set up a project to test. To show our approach with the gomock package, we’ll create a very simple "party" app.
The main service in our app will fetch the names of people coming to the party and print out greeting messages for everyone to the console.
Note: we’ve placed all the source code on github so that you can play around with it. Throughout this article all the code you'll need is pasted here at each step.
Firstly, you'll need to define a visitor:
type Visitor struct {
Name string
Surname string
}
func (v Visitor) String() string {
return fmt.Sprintf("%s %s", v.Name, v.Surname)
}
...and domain-level behaviors:
package party
type Greeter interface {
Hello(name string) string
}
package party
type VisitorGroup string
const (
NiceVisitor VisitorGroup = "nice"
NotNiceVisitor VisitorGroup = "not-nice"
)
type VisitorLister interface {
ListVisitors(who VisitorGroup) ([]Visitor, error)
}
For this tutorial the implementation of the interfaces isn’t necessary. Throughout this Gomock tutorial won’t be using concrete implementation as we try to avoid running the dependencies in unit testing.
Finally, to complete the set up, let's create the service with the method we would test:
package app
import (
"fmt"
"github.com/areknoster/table-driven-tests-gomock/pkg/party"
)
type PartyService struct {
visitorLister party.VisitorLister
greeter party.Greeter
}
func NewPartyService(namesService party.VisitorLister, greeter party.Greeter) *PartyService {
return &PartyService{
visitorLister: namesService,
greeter: greeter,
}
}
func (s *PartyService) GreetVisitors(justNice bool) error {
visitors, err := s.visitorLister.ListVisitors(party.NiceVisitor)
if err != nil {
return fmt.Errorf("could get nice people names: %w", err)
}
if !justNice {
notNice, err := s.visitorLister.ListVisitors(party.NotNiceVisitor)
if err != nil {
return fmt.Errorf("could not get not-nice people's names' ")
}
visitors = append(visitors, notNice...)
}
for _, visitor := range visitors {
fmt.Println(s.greeter.Hello(visitor.String()))
}
return nil
}
You might have noticed that the GreetVisitors method is quite tricky to test. This is because:
it relies on its dependencies.
it prints its result and returns just an error (we cannot just check the return).
it has multiple points of return.
Since we’re unit testing, we’re supposed to exclude dependencies from this equation.
There are many ways to mimic dependency's behavior in Golang. The most basic one being just explicitly writing the returns you need.
You can also save yourself some work and use a package for it. We chose gomock because it allows us to precisely set the functions' invocations and is actively maintained by a proactive community.
Mockgen is a go tool which generates structures that implement given input interfaces. It is installed the same way as other Golang tools. Provided you have everything set up all you need is to:
GO111MODULE=on go get github.com/golang/mock/[email protected]
or
go get github.com/golang/mock/mockgen
if your project happens to be in GOPATH.
Mockgen has two modes. The definitions have been taken from the package repo:
reflect mode 'generates mock interfaces by building a program that uses reflection to understand interfaces.'
Source mode 'generates mock interfaces from a source file. It is enabled by using the -source flag.' source.
From our point of view the main differences are:
the source mode lets you generate unexported interfaces as it statically parses the code.
reflect mode can be used with go:generate annotations. To use these successfully we recommend referring to a great post by Sergey Grebenshchikov.
reflect mode gives you more control over what, where and how it is generated.
Both approaches are correct and will produce the right results in most cases.
For this tutorial we chose to use source mode. Using this approach, you sacrifice precision for a nice and defined structure, and also the possibility of mocking unexported interfaces.
Here is a Makefile which proves to be useful for that:
MOCKS_DESTINATION=mocks
.PHONY: mocks
# put the files with interfaces you'd like to mock in prerequisites
# wildcards are allowed
mocks: pkg/party/greeter.go pkg/party/visitor-lister.go
@echo "Generating mocks..."
@rm -rf $(MOCKS_DESTINATION)
@for file in $^; do mockgen -source=$$file -destination=$(MOCKS_DESTINATION)/$$file; done
Using this Makefile rule, you'll get a mock folder which content directly reflects the structure of the project files:
Resulting structure
As you need to add a new file's content to mocks, add it to mocks prerequisites in Makefile and then make mocks
.
Now, let's get to testing. First, we'll start with a simple, single-case test (non-tabular approach):
func TestPartyService_GreetVisitors_NotNiceReturnsError(t *testing.T) {
// initialize gomock controller
ctrl := gomock.NewController(t)
// if not all expectations set on the controller are fulfilled at the end of function, the test will fail!
defer ctrl.Finish()
// init structure which implements party.NamesLister interface
mockedVisitorLister := mock_party.NewMockVisitorLister(ctrl)
// mockedVisitorLister called once with party.NiceVisitor argument would return []string{"Peter"}, nil
mockedVisitorLister.EXPECT().ListVisitors(party.NiceVisitor).Return([]party.Visitor{{"Peter", "TheSmart"}}, nil)
// mockedVisitorLister called once with party.NotNiceVisitor argument would return nil and error
mockedVisitorLister.EXPECT().ListVisitors(party.NotNiceVisitor).Return(nil, fmt.Errorf("dummyErr"))
// mockedVisitorLister implements party.VisitorLister interface, so it can be assigned in PartyService
sp := &PartyService{
visitorLister: mockedVisitorLister,
}
gotErr := sp.GreetVisitors(false)
if gotErr == nil {
t.Errorf("did not get an error")
}
}
Please refer to gomock documentation for more advanced expectation settings. Now, when the deferred ctrl.Finish()
is called and the expected calls are not made, the tests will fail.
Now let's try to adapt gomock to use in table-driven tests.
We'll start with tabular test template generated by gotests tool:
func TestPartyService_GreetVisitors(t *testing.T) {
type fields struct {
visitorLister party.VisitorLister
greeter party.Greeter
}
type args struct {
justNice bool
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &PartyService{
visitorLister: tt.fields.visitorLister,
greeter: tt.fields.greeter,
}
if err := s.GreetVisitors(tt.args.justNice); (err != nil) != tt.wantErr {
t.Errorf("GreetVisitors() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
As you’ll most likely find, a few problems emerge.
The gomock's design doesn't allow you to both initialize and set up the expected function invocations in a single expression. For this reason, you need to do it "before" test cases.
Also, we want our tests to be idempotent, so each one would need different initialization. As you can imagine, with more than a few test cases it could get quite messy.
Luckily, we’ve managed to find a solution to this. Let's change the structure to the following. All changes are prepended with comments.
func TestPartyService_GreetVisitors(t *testing.T) {
// we embed mocked objects instead of interface, so that we can set up expectations
type fields struct {
visitorLister *mock_party.MockVisitorLister
greeter *mock_party.MockGreeter
}
type args struct {
justNice bool
}
tests := []struct {
name string
// prepare lets us initialize our mocks within the `tests` slice. Oftentimes it also proves useful for other initialization
prepare func(f *fields)
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := fields{
visitorLister: mock_party.NewMockVisitorLister(ctrl),
greeter: mock_party.NewMockGreeter(ctrl),
}
if tt.prepare != nil {
tt.prepare(&f)
}
s := &PartyService{
visitorLister: f.visitorLister,
greeter: f.greeter,
}
if err := s.GreetVisitors(tt.args.justNice); (err != nil) != tt.wantErr {
t.Errorf("GreetVisitors() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Now our tests are fully idempotent thanks to gomock's controller being initialized inside t.Run
and expectations being set for each individual case in prepare function.
Thanks to this feature, you are perfectly equipped to plan exactly what calls are made on your dependencies in each case. If they aren't - you'll get a message indicating which calls are not made.
At this stage it’s time to put everything together and fill in some test cases to test how it works!
tests := []struct {
name string
prepare func(f *fields)
args args
wantErr bool
}{
{
name: "visitorLister.ListVisitors(party.NiceVisitor) returns error, error expected",
prepare: func(f *fields) {
f.visitorLister.EXPECT().ListVisitors(party.NiceVisitor).Return(nil, fmt.Errorf("dummyErr"))
},
args: args{justNice: true},
wantErr: true,
},
{
name: "visitorLister.ListVisitors(party.NotNiceVisitor) returns error, error expected",
prepare: func(f *fields) {
// if given calls do not happen in expected order, the test would fail!
gomock.InOrder(
f.visitorLister.EXPECT().ListVisitors(party.NiceVisitor).Return([]string{"Peter"}, nil),
f.visitorLister.EXPECT().ListVisitors(party.NotNiceVisitor).Return(nil, fmt.Errorf("dummyErr")),
)
},
args: args{justNice: false},
wantErr: true,
},
{
name: " name of nice person, 1 name of not-nice person. greeter should be called with a nice person first, then with not-nice person as an argument",
prepare: func(f *fields) {
nice := []string{"Peter"}
notNice := []string{"Buka"}
gomock.InOrder(
f.visitorLister.EXPECT().ListVisitors(party.NiceVisitor).Return(nice, nil),
f.visitorLister.EXPECT().ListVisitors(party.NotNiceVisitor).Return(notNice, nil),
f.greeter.EXPECT().Hello(nice[0]),
f.greeter.EXPECT().Hello(notNice[0]),
)
},
args: args{justNice: false},
wantErr: false,
},
}
While the function doesn't have enough coverage yet, this example is enough to demonstrate the idea.
Throughout this tutorial we have presented the key ways to tackle the problem of writing precise, robust and explicit table-driven unit tests with the gomock package.
Now that you have a valuable approach you can define test cases in a concise way without any extra work.
Please share if the post helped you!