Test Patterns in Go

New York City Golang Meetup

5 November 2013

Brandon Keene

Microsoft

About me

Disclaimer

Using the testing package

import "testing"

func TestGreeting(t *testing.T) {
    if greeting() != "hello world" {
        t.Fail()
    }
}

Run it:

$ go test hello_test.go
ok    command-line-arguments  0.017s

Woo it passed!

Error checking

func TestCurrentWeather(t *testing.T) {
    weather, err := CurrentWeather("New York, NY")
    if err != nil {
        t.Error(err)
    }

    if weather != "72°F, Sunny" {
        t.Error("current weather incorrect")
    }
}

Most real functions will return some sort of error.

Go is all about explicit error handling and tests are no different.

Error checking

$ go test weather_error_test.go
--- FAIL: TestCurrentWeather (0.00 seconds)
  weather_error_test.go:8: doppler radar offline
  weather_error_test.go:12: current weather incorrect
FAIL
FAIL  command-line-arguments  0.015s

The T.Error function displays the error and continues execution.

weather was also incorrect but we have no idea what value was returned.

Let's modify the test to display this information.

Expected and actual

func TestCurrentWeather(t *testing.T) {
    expected := "72°F, Sunny"
    actual, err := CurrentWeather("New York, NY")
    if err != nil {
        t.Error(err)
    }

    if actual != expected {
        t.Error("expected", expected)
        t.Error("actual  ", actual)
    }
}

The spacing around "actual" helps align output.

Expected and actual

Compared values are now clearly visible in test output.

$ go test weather_expected_test.go
--- FAIL: TestCurrentWeather (0.00 seconds)
  weather_expected_test.go:13: expected 72°F, Sunny
  weather_expected_test.go:14: actual   40°F, Raining
FAIL
FAIL  command-line-arguments  0.015s

A much more helpful error message.

This is great when the test fails months or years from now.

Future you will love past you for it.

Comparing structs

An example using a richer data structure.

func TestCurrentWeather(t *testing.T) {
    expected := &Weather{
        Temperature: 72,
        Conditions:  "Sunny",
        Observed:    time.Date(2013, 11, 05, 16, 20, 0, 0, time.UTC),
    }
    actual, err := CurrentWeather("New York, NY")
    if err != nil {
        t.Error(err)
    }

    if actual != expected {
        t.Error("expected", expected)
        t.Error("actual  ", actual)
    }
}

Comparing structs

$ go test struct_test.go
--- FAIL: TestCurrentWeather (0.00 seconds)
  struct_test.go:18: expected &{72 Sunny 2013-11-05 16:20:00 +0000 UTC}
  struct_test.go:19: actual   &{72 Sunny 2013-11-05 16:20:00 +0000 UTC}
FAIL
FAIL  command-line-arguments  0.016s

Err, what?

They look semantically equivalent but remember they're different instances!

How can we make sure they're equal?

Comparing structs

func TestCurrentWeather(t *testing.T) {
  weather, _ := CurrentWeather("New York, NY")

  if weather.Temperature != 72 {
    t.Error("bad temp")
  }

  if weather.Conditions != "Sunny" {
    t.Error("bad conditions")
  }

  if weather.Observed != time.Date(2013, 11, 05, 16, 20, 0, 0, time.UTC) {
    t.Error("bad observation time")
  }
}

Clearly would work, but it's needlessly verbose.

We can use reflect.DeepEqual to perform a more compact comparison.

Comparing structs

import "reflect"

func TestCurrentWeather(t *testing.T) {
    expected := &Weather{
        Temperature: 72,
        Conditions:  "Sunny",
        Observed:    time.Date(2013, 11, 05, 16, 20, 0, 0, time.UTC),
    }
    actual, err := CurrentWeather("New York, NY")
    if err != nil {
        t.Error(err)
    }

    if !reflect.DeepEqual(actual, expected) {
        t.Error("expected", expected)
        t.Error("actual  ", actual)
    }
}

This also works for arrays, slices, and maps.

Table driven tests

"If the amount of extra code required to write good errors seems repetitive and overwhelming, the test might work better if table-driven, iterating over a list of inputs and outputs defined in a data structure"

From the Go FAQ "Where is my favorite helper function for testing?"

Table driven tests

// Adapted from src/pkg/fmt/fmt_test.go
var fmttests = []struct {
  fmt string
  val interface{}
  out string
}{
  {"%d", 12345, "12345"},
  {"%v", 12345, "12345"},
  {"%t", true, "true"},
}

func TestSprintf(t *testing.T) {
  for _, tt := range fmttests {
    s := Sprintf(tt.fmt, tt.val)
    if s != tt.out {
      t.Errorf("Sprintf(%q, %v) = %q want %q", tt.fmt, tt.val, s, tt.out)
    }
  }
}

Struct literals are super useful.

Using interfaces

Using interfaces

We care more about the behavior of Finder and less about actual database communication.

type Finder struct {
  db *Database
}

type Database interface {
  Select(query) ([]Row, error)
}

func (f *Finder) Names() ([]string, error) {
  rows, err := f.db.Select("SELECT name FROM users")
  if err != nil {
    return nil, err
  }
  return f.process(rows), nil
}

(Note: you should still test real database interaction somewhere)

Using interfaces

type TestDB struct {
  OnSelect func(query string) ([]Row, error)
}

func (db *TestDB) Select(query string) ([]Row, error) {
  return db.OnSelect(query)
}

func TestFinderNamesError(t *testing.T) {
  expected := errors.New("statement invalid")
  db := &TestDB{
    OnSelect: func(query) ([]Row, err) {
      return nil, expected
    },
  }

  finder := &Finder{db}
  _, err := finder.Names()
  if err != expected {
    t.Error("expected", expected)
    t.Error("actual  ", err)
  }
}

Using interfaces

For package level functions, use an underlying variable.

Initialize the real value and swap it out in test.

var db *Database

func init() {
  db = Connect("user@localhost")
}

func Names(query string) ([]string, error) {
  rows, err := db.Select(query)
  //...
}

Redefine functions

Redefine functions

var ErrUnknownLocation = errors.New("unknown location")

func GetWeather(location string) (*Weather, error) {
  code, err := findStationCode(location)
  if err != nil {
    return nil, err
  }
  return queryStation(code)
}

var findStationCode = func(location string) (code int, err error) {
  res, err := http.Get(fmt.Sprintf("https://weather.com/?loc=%s", location))
  ...
}

Redefine functions in tests

func TestUnknownLocation(t *testing.T) {
  findStationCode = func(location string) (code int, err error) {
    if location == "New York, NY" {
      return 10011, nil
    }
    return 0, ErrUnknownLocation
  }

  _, err := GetWeather("Batman, Turkey")
  if err != ErrUnknownLocation {
    t.Error("we should not have found Batman")
    t.Error(err)
  }

  _, err = GetWeather("New York, NY")
  if err != nil {
    t.Error("we should have found New York, c'mon!")
    t.Error(err)
  }
}

Shorthand test types

type Token rune

const (
  Keyword Token = iota
  Wildcard
  Identifier
  EOF
)

type Lexer struct {
  source  string
}

func (l *Lexer) scan() (tokens []Token) {
  var t Token
  for t != EOF {
    t = l.tokenize(l.next())
    tokens = append(tokens, t)
  }
  return tokens
}

Shorthand test types

const (
  K = Keyword
  W = Wildcard
  I = Identifier
  E = EOF
)

func TestScan(t *testing.T) {
  l := &Lexer{`SELECT * FROM users`}
  expected := []Token{K, W, K, I, E}
  actual := l.scan()
  if !reflect.DeepEqual(expected, actual) {
    t.Error("expected", expected)
    t.Error("actual  ", actual)
    t.Error("source  ", l.source)
  }
}

Not too useful in the singular case, but...

Shorthand test types

func TestScan(t *testing.T) {
    cases := []Case{
        Case{
            `SELECT * FROM users`,
            []Token{K, W, K, I, E},
        },
        Case{
            `SELECT * FROM users LIMIT 10 ORDER BY id ASC`,
            []Token{K, W, K, I, K, N, K, K, I, K, E},
        },
        Case{
            `SELECT id, name FROM users WHERE id = 1 AND name BETWEEN("a", "z")`,
            []Token{K, I, C, I, K, I, K, I, O, N, O, I, O, L, S, C, S, R, E},
        },
    }
}

Defintely useful in the plural case.

Memory allocation tests

Memory allocation tests

// Adapted from src/pkg/fmt/fmt_test.go
var mallocBuf bytes.Buffer
var mallocTest = []struct {
  count int
  desc  string
  fn    func()
}{
  {0, `Sprintf("")`, func() { Sprintf("") }},
  {1, `Fprintf(buf, "%s")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%s", "hello") }},
}

func TestCountMallocs(t *testing.T) {
  for _, mt := range mallocTest {
    mallocs := testing.AllocsPerRun(100, mt.fn)
    if got, max := mallocs, float64(mt.count); got > max {
      t.Errorf("%s: got %v allocs, want <=%v", mt.desc, got, max)
    }
  }
}

Using net/http/httptest

import (
  "fmt"
  "log"
  "net/http"
  "net/http/httptest"
)

func TestRecorder(t *testing.T) {
  handler := func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "something failed", http.StatusInternalServerError)
  }

  req, err := http.NewRequest("GET", "http://example.com/foo", nil)
  if err != nil {
    log.Fatal(err)
  }

  w := httptest.NewRecorder()
  handler(w, req)

  fmt.Printf("%d - %s", w.Code, w.Body.String())
}

Using net/http/httptest

func TestServer(t *testing.T) {
  ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, client")
  }))
  defer ts.Close()

  res, err := http.Get(ts.URL)
  if err != nil {
    log.Fatal(err)
  }
  greeting, err := ioutil.ReadAll(res.Body)
  res.Body.Close()
  if err != nil {
    log.Fatal(err)
  }

  fmt.Printf("%s", greeting)
}

Test helper packages

Other frameworks

A word of warning

"[Go] lacks features provided in other language's testing frameworks such as assertion functions."

"...testing frameworks tend to develop into mini-languages of their own, with conditionals and controls and printing mechanisms, but Go already has all those capabilities; why recreate them ?"

"We'd rather write tests in Go; it's one fewer language to learn and the approach keeps the tests straightforward and easy to understand.”

From the Go FAQ "Where is my favorite helper function for testing?"

Don't fight the standard

There are thousands of idiomatic go tests in the standard library.

They test some pretty complex stuff too.

By using your own framework you're ignoring this excellent and prescriptive body of examples.

My opinion: Embrace the Go way.

That said, let's get weird with it!

gocheck

http://labix.org/gocheck
http://godoc.org/launchpad.net/gocheck

gocheck

import (
    . "launchpad.net/gocheck"
    "testing"
    "os"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }


type MySuite struct{}
var _ = Suite(&MySuite{})

func (s *MySuite) TestHelloWorld(c *C) {
    c.Check(42, Equals, "42")
    c.Check(os.Errno(13), Matches, "perm.*accepted")
}

gocheck

$ go test

----------------------------------------------------------------------
FAIL: hello_test.go:16: S.TestHelloWorld

hello_test.go:17:
    c.Check(42, Equals, "42")
... obtained int = 42
... expected string = "42"

hello_test.go:18:
    c.Check(os.Errno(13), Matches, "perm.*accepted")
... value os.Errno = 13 ("permission denied")
... regex string = "perm.*accepted"

OOPS: 0 passed, 1 FAILED
--- FAIL: hello_test.Test
FAIL

Ginko and Gomega

https://github.com/onsi/ginkgo
https://github.com/onsi/gomega

Ginko and Gomega

Describe("ScoreKeeper", func() {
    var scoreKeeper *ScoreKeeper

    BeforeEach(func() {
        scoreKeeper, err := NewScoreKeeper("Denver Broncos")
        Expect(err).NotTo(HaveOccured())
    })

    It("should have a starting score of 0", func() {
        Expect(scoreKeeper.Score).To(Equal(0))
    })

    Context("when a touchdown is scored", func() {
        BeforeEach(func() {
            scoreKeeper.Touchdown("Manning")
        })

        It("should increment the score", func() {
            Expect(scoreKeeper.Score).To(Equal(6))
        })
    })
})

Ginko and Gomega

Resources

Thanks to everyone below!

Slides

http://bit.ly/go-test-patterns-2013

Thank you