Test Patterns in Go
New York City Golang Meetup
5 November 2013
Brandon Keene
Microsoft
Brandon Keene
Microsoft
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!
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.
$ 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.
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.
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.
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) } }
$ 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?
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.
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.
"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?"
// 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.
testing
package does not have mocks or doubles
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)
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) } }
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) //... }
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)) ... }
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) } }
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 }
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...
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.
// 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) } } }
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()) }
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) }
<package>/<package>test
naming scheme to avoid conflicts"[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?"
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!
http://labix.org/gocheck
http://godoc.org/launchpad.net/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") }
$ 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
https://github.com/onsi/ginkgo
https://github.com/onsi/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)) }) }) })
Thanks to everyone below!