Listing interfaces with Go AST for gomock/moq

Testing is an important part of software development. Specifically, mocking is a common approach where the developer implements an interface that “mocks” the behaviour of a concrete implementation. Depending on the complexity of the mocked objects, there are many ways you can approach this problem.

Notably, if you have a simple program, your mocks can be simple as well. Let’s assume that you have an interface which implements a key-value store API. Actually implementing that in Go code is very simple.

type KeyValueDatabase interface { Get(key string) string Set(key string, value string) } type KeyValueDatabaseMock struct { values map[string]string } func (db *KeyValueDatabaseMock) Get(key string) string { return db.values[key] } func (db *KeyValueDatabaseMock) Set(key string, value string) { db.values[key] = value }

The KeyValueDatabaseMock implemented here, should behave as good as a concrete implementation of a real production backing service. Obviously such key value stores aren’t really complex enough to be difficult to mock, but a larger amount of interfaces or more complex ones might be more difficult. Can you imagine mocking a fully-featured SQL client? Me neither.

GoMock

Go has an official mocking framework called GoMock. It can be installed like this:

go get -u github.com/golang/mock/gomock go install github.com/golang/mock/mockgen

With mockgen, you can generate mocks using two different ways. You can generate the mocks for all the interfaces in a given .go file (also known as “source mode”). The other mode of operation is called the “reflect mode”, which uses reflection to understand interfaces and generate mocks for them.

Unfortunately, one needs a way to list interfaces in both modes. You must provide a list of interfaces for reflect mode, and you can’t exclude them in source mode. Looking at another mocking tool, matryer/moq, it also requires you to provide a list of interfaces to generate mock implementations.

Getting interface names

Many of you, and a few people I know personally, quickly resort to some level of bash-fu, or regular expressions to parse the code and retrieve a list of interfaces. And it works well, for the most part. This was a problematic one liner that ended up in my inbox:

$(sed -n 's/^type \([A-Z][^[:space:]]*\) interface .*/\1/p' "interfaces.go")

Of course, when you realize that declarations can be nested inside a type block, the script doesn’t really end up giving you the results you wanted. Nothing a little more of the same doesn’t fix, right?

$(sed -n 's/^\(type \|\s*\)\([A-Z][^[:space:]]*\) interface .*/\2/p' "interfaces.go")

That’s a whole lot better. Except it doesn’t work on your Macbook, because sed there is a different breed (but you can install gnu-sed ). Either way, it’s just a time bomb. For the most obvious one, it doesn’t ignore comments, and it may very well be that an interface could be detected within a comment.

Enter AST

AST stands for an “abstract syntax tree” and consists of data structures that represent your go source code after it has been parsed. Individual AST tokens have different meaning, ranging from declarations of consts, structs, interfaces, functions to opcodes like loops, ifs and returns. And, important to our case, we can parse our source code and list all the exported interface definitions.

The few important packages that we need to parse Go source code are go/ast, go/parser and go/token. We want to produce an application that takes any number of *.go files as arguments, parses them, and outputs a list of all exported interfaces, one per line.

func main() { if len(os.Args) < 2 { fmt.Println("Usage: ./interfaces [file.go, ...]") os.Exit(255) } for _, filename := range os.Args[1:] { if filename == "-" { continue; } fset := token.NewFileSet() node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) if err != nil { fmt.Println("Error parsing file: " + filename) os.Exit(255) } // traverse all tokens ast.Inspect(node, func(n ast.Node) bool { switch t := n.(type) { // find variable declarations case *ast.TypeSpec: // which are public if t.Name.IsExported() { switch t.Type.(type) { // and are interfaces case *ast.InterfaceType: fmt.Println(t.Name.Name) } } } return true }) } }

Breaking it down, the application traverses it’s arguments with a for loop, and then parses each filename into an AST. If - is added as an argument, we skip that argument. This is there so we can trick go run main.go filename.go not to compile filename.go along with our parser, by running it as go run main.go - filename.go .

We can, if parsing of the go source code is successful, then use this AST for traversal. Traversing the AST is very simple. The ast package already provides an utility function, Inspect , which will pass each AST node to our callback for further inspection. What we are doing is:

traversing all the AST nodes, checking if the node is an *ast.TypeSpec (a type declaration), checking if the type declaration is exported, checking the declaration type as *ast.InterfaceType

At that point, all that needs to be done, is to print the interface name to the standard output. This may be further filtered with things like grep and xargs , in order to produce a CSV for gomock or moq.

A short oneliner to produce the CSV argument for gomock or moq, excluding certain interfaces:

# go run cmd/interfaces/main.go - interfaces.go | grep -v ^Permis | paste -s -d, - Roles,Session,Resource,User

Of course, you may pass the options to the generators individually, so you may generate one file per interface instead of a larger file containing all the mocks. It depends on you, and now you finally have a reliable way of doing that.

The full code is available on github. If you liked the article, read below to find out how you can support more of my writing. Thank you for reading.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.