Interfaces in Go

Interfaces are a straight-forward way to decouple behavior from implementation. This post shows how to create reusable tests for testing behavior across implementations.

Interfaces

Interfaces in Go are great: they decouple behavior from implementation. Unlike interfaces in Java implementing an interface in Go does not require the implementing type to state this fact.

This has wide-ranging consequences: as long as a new type implements the behavior specified by an existing interface, the new type just works with the existing code. The existing code requires no knowledge of the new type. Likewise, the new type has no code dependency on the old type. The two pieces are completely decoupled.

Given interfaces as a tool for decoupling it becomes possible to express program behavior just in terms of the abstraction defined by the interface. This is great for several reasons:

  1. we can use pure components, that don't do any IO, in our tests to keep them fast
  2. we can reuse the same set of tests against an interface to test all the implementations of that interface.

The latter is not straightforward in Go. The way the Go testing tools work means we would have to duplicate our test cases for each type we want to test.

An example

Consider the following example: our application needs a key value store for storing user session data. To keep our tests fast, we don't want to hit the disk or a database when using the store. At the moment we're using memcached in our application, but we're contemplating switching to redis

This leaves us with three implementations already: one in memory for the tests, one using memcached for the time being, and one using redis to see how complicated it would be to integrate redis at the moment.

Here's the code we want to test:

type KeyValueStore interface {
        Store(key string, data interface{}) error
        Load(key string, data interface{}) error
}

func LoadSession(req *http.Request, sessionStore KeyValueStore) (*Session, error) {
        sessionId, err := req.Cookie("session")
        if err == http.ErrNoCookie {
                return NewEmptySession(), nil
        }

        session := NewEmptySession()
        err := sessionStore.Load(sessionId, &session)
	if err == ErrKeyNotFound {
        	return NewEmptySession(), nil
	}
	if err != nil {

                return nil, err
        }

        return session, nil
}

func SaveSession(session *Session, sessionStore KeyValueStore) error {
        return sessionStore.Store(session.Id(), session)
}

The implemenation of the Session type is omitted, because it is not important for this example. Looking at the code, we can see this behavior of a KeyValueStore:

This can be translated directly into test cases:

func TestKeyValueStore_Load_retrievesStoredValue(t *testing.T) {
        store := GetANewKeyValueStoreFromSomewhere()

        if err := store.Store("the-key", "the-value"); err != nil {
                t.Fatal(err)
        }

        loaded := ""
        if err := store.Load("the-key", &loaded); err != nil {
                t.Fatal(err)
        }

        if got, want := loaded, "the-value"; got != want {
                t.Errorf(`loaded = %v; want %v`, got, want)
        }
}

func TestKeyValueStore_Load_returnsErrKeyNotFound_ifKeyDoesNotExist(t *testing.T) {
        store := GetANewKeyValueStoreFromSomewhere()

        loaded := ""
        err := store.Load("the-key", &loaded)

        if got, want := err, ErrKeyNotFound; got != want {
                t.Errorf(`err = %v; want %v`, got, want)
        }
}

Note the call to GetANewKeyValueStoreFromSomewhere. This is a placeholder, because we need to run the same two tests three times, once for each implementation. How can we run the tests for each implementation, supplying a different value for the placeholder on each run?

There are several possibilities, amongst others:

Using TestMain

Defining TestMain requires the least amount of code:

var GetANewKeyValueStoreFromSomewhere func() KeyValueStore

func TestMain(m *testing.T) {
        stores := []struct {
                name string
                constructor func() KeyValueStore
        }{
                {"MemoryKeyValueStore", MustCreateMemoryKeyValueStore},
                {"MemcachedKeyValueStore", MustCreateMemcachedKeyValueStore},
                {"RedisKeyValueStore", MustCreateRedisKeyValueStore},
        }

        flag.Parse()

        for _, store := range stores {
                // Log store name so that we know for which store the
                // tests failed.
                log.Printf("Testing %s", store.name)

                // Override constructor used by tests
                GetANewKeyValueStoreFromSomewhere = store.constructor

                if code := m.Run(); code != 0 {
                        os.Exit(code)
                }
        }
}

This runs the test suite in a loop, using a different function for creating the key value store each time. Adding a new test subject is easy: just add another line to the stores slice. One drawback is that no new tests can be added outside of the current package.

Using a custom type

By introducing a new type to represent our test case, we can make it possible for code outside of this package to reuse the same test suite.

An instance of this type holds all the necessary references we need: a function for constructing a store instance and a function for doing any cleanup, such as removing keys from the store before the next run.

type KeyValueStoreTests struct {
        SetUp func() KeyValueStore
        TearDown func()
}

func (self *KeyValueStoreTests) Run(t *testing.T) {
        self.testKeyValueStore_Load_retrievesStoredValue(t)
        self.testKeyValueStore_Load_returnsErrKeyNotFound_ifKeyDoesNotExist(t)
}

func (self *KeyValueStoreTests) testKeyValueStore_Load_retrievesStoredValue(t *testing.T) {
        store := self.SetUp()
        defer self.TearDown()
        t.Logf("testKeyValueStore_Load_retrievesStoredValue %T", store)
        // ...
}

The actual test setup and assertions are the same as in the previous example. We also log the type of the created store to get information about which store fails the test.

On its own, above code doesn't run yet. We still need to write driver functions for each type we want to test:

func TestMemcachedKeyValueStore_behavesLikeKeyValueStore(t *testing.T) {
        suite := &KeyValueStoreTests{
                SetUp: MustCreateMemcachedKeyValueStore,
                TearDown: func() { /* do nothing */ },
        }

        suite.Run(t)
}

Conclusion

Go's interfaces facilitate decoupling and make it easy to write pluggable components. The techniques described in this article make it easy to cover all implementations of an interface with tests.