# Go testing patterns --- ## About me - Eng. Tech Lead / SRE @ Cisco - I write Go for a living (for 5+ years now) - Love OSS - Obsessed with DX https://sagikazarmark.hu https://about.sagikazarmark.hu [@sagikazarmark](https://twitter.com/sagikazarmark) --- ## Basics -- ### Builtin test framework ```shell $ go test ok pkg/pets 1.303s ok pkg/store 0.037s ok pkg/store/storeadapter 0.025s ok pkg/store/payment 0.111s ``` -- ### Anatomy of a test files ending with `_test.go` functions starting with `Test[A-Z]` `func(t *testing.T)` signature -- ```go package pets import "testing" func TestDogBarks(t *testing.T) { dog := NewAnimal(Dog) if dog.Barks() == false { // a dog should bark } } ``` -- ### Test output/result - `t.Log()` - `t.Error()` - `t.Fatal()` - `t.Skip()` -- ### Parallel test running Tests are executed _concurrently_ by default. Packages are tested _in parallel_ by default. ```go import "testing" func TestSomething(t *testing.T) { t.Parallel() } ``` -- ### Running tests ```bash # Run tests $ go test # Run tests for specific packages $ go test ./pkg/... # Run tests in verbose mode to see every output $ go test -v # Run a specific test $ go test -run ^TestIntegration$ # Run tests with the race detector $ go test -race ``` -- ### Test types - Test - Benchmark - Example --- ## Writing tests -- ```go func TestDogBarks(t *testing.T) { dog := NewAnimal(Dog) if dog.Barks() == false { t.Error("a dog should be able to bark") } } ``` -- ```go func TestGetDog(t *testing.T) { dog, err := getDog(1) if err != nil { // oneliner? t.Fatal(err) } if got, want := dog.ID, 1; got != want { // oneliner? t.Errorf("id mismatch\nactual: %+v\nexpected: %+v", got, want) } } ``` -- ### Helpers ```go func noError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatal(err) } } ``` ```go func TestGetDog(t *testing.T) { dog, err := getDog(1) noError(t, err) // ... } ``` -- ### Assertions Testify: https://github.com/stretchr/testify ```go import "github.com/stretchr/testify/assert" import "github.com/stretchr/testify/require" func TestGetDog(t *testing.T) { dog, err := getDog(1) require.NoError(t, err) assert.Equal(t, 1, dog.ID) } ``` -- ### Dependencies ```go type DogService struct { Repository DogRepository } type Dog struct { ID string // ... } type DogRepository interface { GetDog(id string) (Dog, err) } ``` -- ### Test doubles: stubs ```go type inmemDogRepository struct{ dogs map[string]Dog } func (r inmemDogRepository) GetDog(id string) (Dog, error) { dog, ok := r.dogs[id] if !ok { return Dog{}, ErrDogNotFound } return dog, nil } ``` -- ### Test doubles: mocks Testify: `github.com/stretchr/testify/mock` ```go func TestFoo(t *testing.T) { foo := new(Foo) foo.On("DoSomething", 123).Return(true, nil) bar(foo) foo.AssertExpectations(t) } ``` -- ### Test doubles: generated mocks Generator: https://github.com/vektra/mockery ```go type Stringer struct { mock.Mock } func (m *Stringer) String() string { ret := m.Called() var r0 string if rf, ok := ret.Get(0).(func() string); ok { r0 = rf() } else { r0 = ret.Get(0).(string) } return r0 } ``` --- ## Test organization -- ### Test per feature ```go package pets import "testing" func TestDogBarks(t *testing.T) { dog := NewAnimal(Dog) if dog.Barks() == false { // a dog should bark } } func TestDogBites(t *testing.T) { // ... } ``` -- ### Subtests ```go package pets import "testing" func TestDog(t *testing.T) { dog := NewAnimal(Dog) t.Run("barks", func(t *testing.T) { // ... }) t.Run("bites", func(t *testing.T) { // ... }) } ``` ```shell $ go test -run TestDog/barks ``` -- ### Test suites Testify: `github.com/stretchr/testify/suite` ```go func TestDogTestSuite(t *testing.T) { suite.Run(t, new(DogTestSuite)) } type DogTestSuite struct { suite.Suite } func (s *DogTestSuite) SetupTest() {} func (s *DogTestSuite) AfterTest(suiteName, testName string) {} func (s *DogTestSuite) Test_Barks() { // ... } ``` -- ### Table driven tests ```go func TestSplit(t *testing.T) { tests := struct{ name string input string sep string want []string } { { name: "empty", input: "", sep: ",", want: []string{""}, }, } } ``` -- ### Table driven tests ```go func TestSplit(t *testing.T) { // ... for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { assert.Equal(t, Split(tc.input, tc.sep), tc.want) }) } } ``` --- ## Integration tests -- ### Build tags ```go //go:build integration package foo func TestFoo(t *testing.T) { // ... } ``` ```shell $ go test -tags integration ``` https://peter.bourgon.org/blog/2021/04/02/dont-use-build-tags-for-integration-tests.html -- ### Short tests ```go package foo func TestFoo(t *testing.T) { if t.Short() { t.Skip("skipping test in short mode") } // ... } ``` ```shell $ go test -short ``` -- ### Environment variables ```go func TestFoo(t *testing.T) { fooAddr := os.Getenv("FOO_ADDR") if fooAddr == "" { t.Skip("set FOO_ADDR to run this test") } f, err := foo.Connect(fooAddr) // ... } ``` ```shell $ FOO_ADDR=127.0.0.1:8080 go test ``` -- ### Test name ```go func TestIntegration(t *testing.T) { if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name()) { t.Skip("skipping as execution was not requested explicitly using go test -run") } t.Run("foo", testFoo) // ... } func testFoo(t *testing.T) { // ... } ``` ```shell $ go test -run ^TestIntegration$ ``` --- ## "Tips" Take them with a grain of salt. -- ### CI: Race detector ```go $ go test -race ``` -- ### CI: Code coverage https://github.com/gotestyourself/gotestsum -- ### Avoid "test frameworks" - Javaism, Rubyism - Most libraries: https://awesome-go.com/#testing - Just use Testify - Exception: godog/go-bdd --- ## What's new -- ### Go 1.16: `testing/fstest` - Filesystem tests - `MapFS` -- ### Go 1.17: `TB.Setenv` ```go func TestFoo(t *testing.T) { t.Setenv("KEY", "VALUE") } ``` Must **not** be used in parallel tests (ie. with `t.Parallel`). -- ### Go 1.18: Fuzzing ```go func FuzzParseQuery(f *testing.F) { f.Add("x=1&y=2") f.Fuzz(func(t *testing.T, queryStr string) { // ... }) } ``` https://go.dev/blog/fuzz-beta --- # The End Questions? https://sagikazarmark.hu [@sagikazarmark](https://twitter.com/sagikazarmark)