Tech and travel

Adding a Go interface to help with unit testing

2018-08-15

Go interfaces apply automatically to any types that implement all the methods. This means you can define your interfaces near the code that uses them. They also provide an easy way of stubbing out code for use in unit tests. Here’s an example.

An existing method that consumes a given amount exists on a type:

func (o Object) Consume(amount uint32) (uint32, error) {
  /* Implementation is not really important */
}

It is used like this:

func UserOfConsume () {
  /* Existing code */

  retAmt, err := o.Consume(amt)

  /* Existing code */
}

This method has a problem though. It throws an error when the given amount is too high. We can’t change the Consume() method. The solution was to replace it with this:

func UserOfConsume () {
  /* Existing code */

  retAmt, err := bisection.Checkout(o.Consume, amt) 

  /* Existing code */
}

The new bisection.Checkout() function tries the given method first with the given parameter. If that doesn’t work it calls the method again with half of the given amount. If that fails again it uses half of the previously used amount again. This is repeated a given number of times.

First an interface that matches the existing code is defined in the new bisection package. This interface is given as a parameter to the new Checkout() function where it is called at some point. Checkout() wraps the existing function as it were.

// bisection.go
package bisection

type Consumer interface {
  Consume(amount uint32) (uint32, error)
}

func Checkout(cf Consumer, amt uint32) (uint32, error) {
  // Bisection code: not pertinent
  retAmt, err := cf.Consume(amt)
  // remainder of Bisection code: not pertinent
}

Having the interface makes testing a lot easier. A stub type can be created that implements the interface. This can be used as input in a unit test package as such:

// bisection_test.go
package bisection

import (
  "testing"
)

type MockConsumer struct {
  total uint32
}

func (m *MockConsumer) Consume(amount uint32) (uint32, error) {
  // Code that simulates the real thing
}

// The unit tests go here

Because MockConsumer implements the Consumer interface it can be given as input to bisection.Checkout() in the unit tests.

Copyright (c) 2024 Michel Hollands