Go Interface Generation
Pascal Dennerly
Posted on February 20, 2020
Go Interface Generation
I’d like to rave about a tool that I discovered recently. But first…a little history.
I’ve been working with Buffalo and enjoying it very much. It’s all there in front of you, in Go (my very favourite language) and it’s easy to take your service from the basic out-of-the-box template to adding your own domain models and business logic.
But I also have a background in Java of the Big Business kind. We’re talking full DDD and 3-tier applications in web containers, JCA components and fancy messaging solutions.
Now the thing I like about 3-tier applications is the strong seperation of concerns. Buffalo, as easy as it is, doesn’t have this same seperation. Buffalo deals with this by making strong use of integration tests using a full database implementation and utilities to manage database schemas and populating the database with data according to what is defined in your text fixtures.
In short, I want to be able to take the business logic out of the handler and put it in a repository that I can abstract away so that when I do my testing (first), I can make sure that I’m only testing 1 thing at a time.
How do we achieve this in Buffalo?
Well the obvious way is to move the logic for making queries against the database in to its own type, passing in the dependencies as members of this class. Stub out the dependencies - a route for validating outputs against the inputs that we insert.
Buffalo makes extensive use of the incredibly useful package Pop. Unfortunately, access to the database in this package is via the Connection type. It’s unfortunate because it is a struct
, which means that we can’t just insert a mock version of it in to the units that we’d like to test.
To perform this injection of a mock, we need to create an interface that exposes each of the exported functions on the Connection
type.
After a little searching, I found ifacemaker
. This tools reads the struct and creates an interface that allows access to each of the exported functions on that type.
To run the tool, I did this:
ifacemaker --file ~/go/src/github.com/gobuffalo/pop/connection.go --struct Connection --iface DBConnection --pkg repo
This presented the following type:
package repo
// DBConnection ...
type DBConnection interface {
String() string
// URL returns the datasource connection string
URL() string
// MigrationURL returns the datasource connection string used for running the migrations
MigrationURL() string
// MigrationTableName returns the name of the table to track migrations
MigrationTableName() string
// Open creates a new datasource connection
Open() error
// Close destroys an active datasource connection
Close() error
// Transaction will start a new transaction on the connection. If the inner function
// returns an error then the transaction will be rolled back, otherwise the transaction
// will automatically commit at the end.
Transaction(fn func(tx *Connection) error) error
// NewTransaction starts a new transaction on the connection
NewTransaction() (*Connection, error)
// Rollback will open a new transaction and automatically rollback that transaction
// when the inner function returns, regardless. This can be useful for tests, etc...
Rollback(fn func(tx *Connection)) error
// Q creates a new "empty" query for the current connection.
Q() *Query
// TruncateAll truncates all data from the datasource
TruncateAll() error
}
We can now use this in our Go mocking tools - for example github.com/stretchr/testify/mock
. But if we’re going to use a mock, we have to generate one. Here’s where we turn to mockery. The first time you use it - it’ll fail. This is because it doesn’t populate the package imports and package names (for example, Connection
rather than pop.Connection
). Fortunately this is easy to fix, giving this:
package repo
import (
"github.com/gobuffalo/pop"
)
// DBConnection represents a Buffalo pop connection to a database
type DBConnection interface {
String() string
// URL returns the datasource connection string
URL() string
// MigrationURL returns the datasource connection string used for running the migrations
MigrationURL() string
// MigrationTableName returns the name of the table to track migrations
MigrationTableName() string
// Open creates a new datasource connection
Open() error
// Close destroys an active datasource connection
Close() error
// Transaction will start a new transaction on the connection. If the inner function
// returns an error then the transaction will be rolled back, otherwise the transaction
// will automatically commit at the end.
Transaction(fn func(tx *pop.Connection) error) error
// NewTransaction starts a new transaction on the connection
NewTransaction() (*pop.Connection, error)
// Rollback will open a new transaction and automatically rollback that transaction
// when the inner function returns, regardless. This can be useful for tests, etc...
Rollback(fn func(tx *pop.Connection)) error
// Q creates a new "empty" query for the current connection.
Q() *pop.Query
// TruncateAll truncates all data from the datasource
TruncateAll() error
}
The next time we run mockery
, it magically works. Take a look at this snippet, if it compiles then you have yourself an interface over a struct
to unlock your unit tests.
The following code should work, although you may need to fill in the blanks:
func AcceptConnection(tx DBConnection) {}
func TestMocks(t *testing.T) {
tx := &pop.Connection{}
mx := &mocks.DBConnection{}
AcceptConnection(tx)
AcceptConnection(mx)
}
If you get this far then you have all of the components needed to add a testable repository tier in your Buffalo app.
Posted on February 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.