Interface With Your Database in Go Tests

Caleb Hearth

Go’s strong type system includes a concept called interface. In Go, an interface is a collection of function signatures. Any type that implements all of those functions is implicitly able to be used as an interface type.

For example, Go’s empty interface, interface{}, is similar to Ruby’s BasicObject or Java’s Object, from which all classes inherit in these languages.

More concretely, Ruby’s Comparable requires a #<=> method to be defined. To represent that in Go, we might define these interfaces as:

type Comparable interface {
  // Compare accepts another Comparable of the same type and returns an int
  // value representing whether the other is less than (-1), greater than (1),
  // or equal to (0) the callee.
  // Compare returns an error if the Comparable argument is of a different type
  // than the callee and cannot be compared.
  Compare(Comparable) (int, error)
}

We could then use any type that implemented Comparable in methods defined to operate on Comparable:

func (self int) Compare(other Comparable) (int, error) {
  switch other.(type) {
  case int:
  case float64:
    other = int(other)
  case string:
    other, err = strconv.Atoi(other)
    if err != nil {
      return 0, err
    }
  default:
    return 0, fmt.Errorf("%v is not comparable to type int.")
  }

  if other > self {
    return 1, nil
  } else if other == self {
    return 0, nil
  } else {
    return -1, nil
  }
}

func Equal(first, second Comparable) (bool, err) {
  if result, err := first.Compare(second); err != nil {
    return false, err
  }

  return result == 0, nil
}

Okay, that’s out of the way. Now let’s look at how we can use interfaces to abstract away a database in tests.

We’ll define a DataAccessLayer interface which includes signatures for the methods we’ll need:

type DataAccessLayer interface {
  FindAuthor(int) Author
  FindPostsForAuthor(Author) []Post
  FindCommentsForPost(Post) []Comment
}

Building to that interface, we’d define a type postgres that included code to retreive each object or collection from a database connection it maintains internally.

But for our unit tests, we don’t want to ever touch the database. By the power of interface, we can define a dummy TestDAL that returns known values so that we can test DataAccessLayer‘s collaborators in relative isolation.

type TestDAL {
  Author   Author
  Posts    []Post
  Comments []Comment
}

func (t *TestDAL) FindAuthor(int) Author {
  return t.Author
}

func (t *TestDAL) FindPostsForAuthor(Author) []Post {
  return t.Posts
}

func (t *TestDAL) FindCommentsForPost(Post) []Comment {
  return t.Comments
}

We ignore the arguments because we initialize the TestDAL with the result we want, so they’re not needed.

In our test, we can create just the amount of data we need:

func TestDALCollaborator(t *testing.T) {
  dal := TestDAL{Author: Author{}}
  collaborator := Collaborator{DAL: dal}

  result := collaborator.FunctionNeedingAnAuthor()

  // Some verification
}

And we never need to hit an external dependency like the Postgres database in our test, keeping it blazing fast.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.