loading...

Go Interface Generation

dnnrly profile image Pascal Dennerly Originally published at skillscheck.co.uk on ・4 min read

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 by:

dnnrly profile

Pascal Dennerly

@dnnrly

Extremely privileged to have had the opportunities that I did. Backend developer with mphhh-mumble years experience of 'Enterprise'.

Discussion

pic
Editor guide