Mocking using interfaces

Nearly every piece of software is composed of multiple components which work together to provide a set of functions. The division into components has many benefits, one of which is smaller scope of tests, but to fully leverage it we need to be able to isolate the components in the test environment. Every programming language provides different tools to accomplish this isolation.

Let’s look at an example in Go. One of our programs includes a registry of devices represented by a serial number. For simplicity, let’s assume the registry provides only add and remove operations. In the land of Go, it will probably take a form of a struct type and a set of methods to operate it. While we could go ahead and implement it, there is a benefit to defining the public interface of the registry as an interface type:

type Registrar interface {
  Register(SerNo) error
  Remove(SerNo) error
}

Because interfaces in Go are implemented implicitly, we can write our implementation DeviceRegistry as we wanted to do anyway. The difference is that we have an extra layer in the form of an interface type. All functions which previously took DeviceRegistry as an argument, for example an HTTP handler:

func CreateRegisterHandler(registry DeviceRegistry) func(http.Handler) http.Handler {
  return func(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {

      serNo := context.Get(r, "serno")

      err := registry.Register(SerNo(serNo))
      HandleError(r, w, err)

      next.ServeHTTP(w, r)
    }
    return http.HandlerFunc(fn)
  }
}

will change to accept the interface type instead:

func CreateRegisterHandler(registry Registrar) func(http.Handler) http.Handler {
  // ...
}

This minor change allows us to fully control the behavior of the registry in tests of CreateRegisterHandler — instead of passing a real DeviceRegistry to the function being tested, we pass a dummy implementation.

type RegistryMock struct {
  duplicityError   bool
  notFoundError    bool
}


func (r *RegistryTest) Register(sn SN) error {
  if r.duplicityError {
    return newErr(DuplicityError, "SerNo already exists")
  }
  return nil
}

func (r *RegistryTest) Remove(sn SN) error {
  if r.notFoundError {
    return newErr(SerNoNotFoundError, "SerNo not found")
  }
  return nil
}

RegistryMock implements the methods defined in Registrar interface, therefore it implements that interface and can be passed anywhere where Registrar is expected:

func TestCreateRegisterHandlerAddValidRequest(t *testing.T) {
  // create mock registry
  registry := RegistryTest{}

  // create a response recorder and call the function under test
  serno := "50-6555-8944"
  nextCalled, w := createSernoHandlerWithResponseRecorder(t, &registry, serno)

  expectNextCalled(t, nextCalled)
  expectSuccess(t, w)
}

func createSernoHandlerWithResponseRecorder(
  t *testing.T, 
  registry *Registrar, 
  serNo string) (bool, *httptest.ResponseRecorder) {
  // create a request
  r, err := http.NewRequest("POST", "/register", nil)
  if err != nil {
    t.Fatal(err)
  }

  // set up parameters extracted from the URL in another layer
  context.Set(r, "serno", serNo)
  
  // prepare a recorder
  w := httptest.NewRecorder()

  // create next handler in the chain that serves as a spy
  nextCalled := false
  inner := func(w http.ResponseWriter, r *http.Request) {
    nextCalled = true
  }
  
  // call the function under test
  CreateRegisterHandler(registry)(http.HandlerFunc(inner)).ServeHTTP(w, r)
  return nextCalled, w
}

This test case tests for successful registration, therefore all error flags on the mock registry are turned off. Other tests would set the available flags for forcing various errors and expect appropriate behaviour from the handler.

By defining an interface type we can easily create mock implementations — that we have complete control over — for use in tests. Because interfaces are implemented implicitly, there is no overhead (and no multiple inheritance conflicts).

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus