Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The art of writing idiomatic test code / Liron Levin

The art of writing idiomatic test code / Liron Levin

There are numerous guides, talks and discussions on writing idiomatic go code, however, clear guidelines on writing quality unit tests in go are sparse. In this talk, we will show you best-practices from popular-open source projects for writing clean, extendable and idiomatic test code.

There are numerous guides, talks and discussions on writing idiomatic go code, however, clear guidelines on writing quality unit tests in go are sparse. Should you use a test framework or rely on native go testing package? Should you hand-write your mocks? When should you use data-driven tests and how to write clear asserts?
In this talk, we will provide a short introduction to writing solid unit tests in go. We will use examples from popular open source projects to demonstrate how leveraging test tools and test pattern can improve readability, speed and quality of your project.
We will discuss: Common testing terminology Advantages and disadvantages of popular test framework Methods to incorporate test tools and auto-generated code in your build pipeline Writing elegant data-driven tests in go.
In the end, attendees will have solid understanding on how to write clean idiomatic tests, what are the best-tools for the trade and how to incorporate them in your build pipeline.

GopherCon Israel

February 11, 2019
Tweet

More Decks by GopherCon Israel

Other Decks in Programming

Transcript

  1. Why should you write unit tests? • Find bugs early

    • Regression • Easier to change code • Document and structure your API • Contributing to open source
  2. Testing in open source Project Code lines Tests lines Docker

    (moby) 138158 56% 110,258 44% Kubernetes 1,238,231 70% 525,730 30% Ethereum 220,452 71% 90,839 29% Influxdb 137,308 63% 80,978 37%
  3. STORY TIME We recently joined a company called complexify.io -

    a new, innovative company with the moto of making simple things complex.
  4. STORY TIME As our first task, we were asked to

    build the world’s first fibonacci service. CLI Fibonacci service Fibonacci DB
  5. STORY TIME As this is not a trivial project, we

    were assigned a supportive tech-lead
  6. As this is not a trivial project, we were assigned

    a supportive tech-lead STORY TIME BOB!
  7. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { DONE
  8. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) DONE
  9. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) if err != nil { return nil, err } return &fibonaccier{db:dbClient}, nil } DONE
  10. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) if err != nil { return nil, err } return &fibonaccier{db:dbClient}, nil } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.client.Fib(n) } DONE
  11. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) if err != nil { return nil, err } return &fibonaccier{db:dbClient}, nil } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.client.Fib(n) } package server import "testing" func TestFibonaccierFib(t *testing.T) { } DONE
  12. SERVER package db func MkClient(connectionString string) (*Client, error) { //

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) if err != nil { return nil, err } return &fibonaccier{db:dbClient}, nil } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.client.Fib(n) } package server import "testing" func TestFibonaccierFib(t *testing.T) { fib, err := MkFibonaccier(/* ??? */) } DONE
  13. DEPENDENCY INJECTION Instead of having your objects creating a dependency

    ..., you pass the needed dependencies in to the object externally, and you make it somebody else's problem …. https://stackoverflow.com/questions/130794/what-is-dependency-injection
  14. containerd runtime/v2/containerd/manager.go func New( ctx context.Context, root, state, containerdAddress string,

    events *exchange.Exchange, db *metadata.DB) (*TaskManager, error) { DEPENDENCY INJECTION - OPEN SOURCE
  15. SERVER - DEPENDENCY INJECTION package db func MkClient(connectionString string) (*Client,

    error) { // Done! } package server func NewFibonaccier(db *db.Client) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } DONE
  16. SERVER - DEPENDENCY INJECTION package db func MkClient(connectionString string) (*Client,

    error) { // Done! } package server func NewFibonaccier(db *db.Client) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server import "testing" func TestFibonaccierFib(t *testing.T) { DONE
  17. SERVER - DEPENDENCY INJECTION package db func MkClient(connectionString string) (*Client,

    error) { // Done! } package server func NewFibonaccier(db *db.Client) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server import "testing" func TestFibonaccierFib(t *testing.T) { db, err := db.MkClient(/*???*/) // ... fib := NewFibonaccier(db) } DONE
  18. containerd runtime/v2/containerd/manager.go func New( ctx context.Context, root, state, containerdAddress string,

    events *exchange.Exchange, db *metadata.DB) (*TaskManager, error) { DEPENDENCY INJECTION - OPEN SOURCE go-ethereum swarm/network/stream/delivery.go func NewSwarmChunkServer( chunkStore storage.ChunkStore) *SwarmChunkServer { ... }
  19. SERVER - INTERFACE INJECTION package server type DBClient interface {

    Fib(n uint64) (big.Int, error) } package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) }
  20. SERVER - INTERFACE INJECTION package server type DBClient interface {

    Fib(n uint64) (big.Int, error) } package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server import "testing" func TestFibonaccierFib(t *testing.T) { db := /* ??? */ fib := NewFibonaccier(db) }
  21. MOCKS In short, mocking is creating objects that simulate the

    behavior of real objects. https://stackoverflow.com/questions/2665812/what-is-mocking
  22. kubernetes taging/src/k8s.io/apiserver/plugin/pkg/authorizer/we bhook/webhook_test.go type mockService struct { allow bool statusCode

    int called int } func (m *mockService) Review(r *v1beta1.SubjectAccessReview) { m.called++ r.Status.Allowed = m.allow } func (m *mockService) Allow() { m.allow = true } func (m *mockService) Deny() { m.allow = false } func (m *mockService) HTTPStatusCode() int { return m.statusCode } MOCKS - HAND-WRITTEN VS AUTOGEN
  23. kubernetes taging/src/k8s.io/apiserver/plugin/pkg/authorizer/we bhook/webhook_test.go type mockService struct { allow bool statusCode

    int called int } func (m *mockService) Review(r *v1beta1.SubjectAccessReview) { m.called++ r.Status.Allowed = m.allow } func (m *mockService) Allow() { m.allow = true } func (m *mockService) Deny() { m.allow = false } func (m *mockService) HTTPStatusCode() int { return m.statusCode } MOCKS - HAND-WRITTEN VS AUTOGEN kubernetes pkg/kubelet/dockershim/network/testing/mock_network_plu gin.go import ( gomock "github.com/golang/mock/gomock" ) // Generated code, generated via: `mockgen k8s.io/kubernetes/pkg/kubelet/network NetworkPlugin > $GOPATH/src/k8s.io/kubernetes/pkg/kubelet/network/testing/mock_ network_plugin.go` // Edited by hand for boilerplate and gofmt. // TODO, this should be autogenerated/autoupdated by scripts. func NewMockNetworkPlugin(ctrl *gomock.Controller) *MockNetworkPlugin { ... }
  24. Hand written • Pros ◦ Simple ◦ Easy to set

    up • Cons ◦ Boilerplate code (and more bugs) ▪ Expectations ▪ Validation ◦ Limited interface ◦ Hard to enforce testing conventions MOCKS - HAND-WRITTEN VS AUTOGEN Auto-generated • Pros ◦ Battle tested ◦ Rich and fluent API • Cons ◦ Have to incorporate into build ◦ Opinionated API
  25. Hand written • Pros ◦ Simple ◦ Easy to set

    up • Cons ◦ Boilerplate code (and more bugs) ▪ Expectations ▪ Validation ◦ Limited interface ◦ Hard to enforce testing conventions MOCKS - HAND-WRITTEN VS AUTOGEN Auto-generated • Pros ◦ Battle tested ◦ Rich and fluent API • Cons ◦ Have to incorporate into build ◦ Opinionated API
  26. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server type DBClient interface { Fib(n int) (big.Int, error) }
  27. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server type DBClient interface { Fib(n int) (big.Int, error) } package mock // MockDBClient is a mock of DBClient interface type MockDBClient struct { … } // Fib mocks base method func (m *MockDBClient) Fib(n int) (big.Int, error) { // Fib indicates an expected call of Fib func (mr *MockDBClientMockRecorder) Fib(n interface{}) *gomock.Call { ...
  28. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) }
  29. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server func TestFibonaccierFib(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()
  30. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server func TestFibonaccierFib(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mock := mock.NewMockDBClient(ctrl) mock.EXPECT(). Fib(10). Times(1). DoAndReturn(func(n uint64) (big.Int, error) { Return *big.NewInt(55), nil })
  31. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go package server func NewFibonaccier(db DBClient) *fibonaccier { return &fibonaccier{db:db} } func(f *fibonaccier) Fib(n int) (big.Int, error) { return f.db.Fib(n) } package server func TestFibonaccierFib(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mock := mock.NewMockDBClient(ctrl) mock.EXPECT(). Fib(10). Times(1). DoAndReturn(func(n uint64) (big.Int, error) { Return *big.NewInt(55), nil }) fib := NewFibonaccier(mock) ret, err := fib.Fib(10) /* ??? */ }
  32. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ...
  33. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) T Require
  34. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) if err != nil { t.Errorf("expected no error got %s", err) } func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) require.NoError(t, err) T Require
  35. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) if err != nil { t.Errorf("expected no error got %s", err) } if big.NewInt(55).Cmp(&ret) != 0 { t.Errorf("expected 55 got %v", *big.NewInt(55), ret) } func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) T Require
  36. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) if err != nil { t.Errorf("expected no error got %s", err) } if big.NewInt(55).Cmp(&ret) != 0 { t.Errorf("expected 55 got %v", *big.NewInt(55), ret) } _, err = fib.Fib(-5) func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) _, err = fib.Fib(-5) T Require
  37. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) if err != nil { t.Errorf("expected no error got %s", err) } if big.NewInt(55).Cmp(&ret) != 0 { t.Errorf("expected 55 got %v", *big.NewInt(55), ret) } _, err = fib.Fib(-5) if err == nil { t.Error("expected error got nil") } func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) _, err = fib.Fib(-5) require.Error(t, err) T Require
  38. ASSERTIONS func(fibonaccier *fibonaccier) Fib(n int) (big.Int, error) { if n

    < 0 { // Undefined return big.Int{}, fmt.Errorf("input %d is too low", n) } if n >= 10000 { // 2000-digit fibonacci number return big.Int{}, fmt.Errorf("input %d is too high", n) } ... func TestFibonaccierFib(t *testing.T) { // Setup ret, err = fib.Fib(10) if err != nil { t.Errorf("expected no error got %s", err) } if big.NewInt(55).Cmp(&ret) != 0 { t.Errorf("expected 55 got %v", *big.NewInt(55), ret) } _, err = fib.Fib(-5) if err == nil { t.Error("expected error got nil") } func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10) require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) _, err = fib.Fib(-5) require.Error(t, err) _, err = fib.Fib(20000) require.Error(t, err) } T Require
  39. Assertion library • Pros ◦ Compact ◦ Reduce mental burden

    when reviewing tests • Cons ◦ Opinionated ◦ Error might be less verbose Native - T • Pros ◦ Verbose • Cons ◦ Readability and maintenance ◦ Copy-pasting errors ASSERTIONS - T VS LIBRARY
  40. Assertion library • Pros ◦ Compact ◦ Reduce mental burden

    when reviewing tests • Cons ◦ Opinionated ◦ Error might be less verbose Native - T • Pros ◦ Verbose • Cons ◦ Readability and maintenance ◦ Copy-pasting errors ASSERTIONS - T VS LIBRARY
  41. func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10)

    require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) // More Setup _, err = fib.Fib(-5) require.Error(t, err) LONG TEST - ZOOM IN
  42. func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10)

    require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) // More Setup _, err = fib.Fib(-5) require.Error(t, err) // More Setup _, err = fib.Fib(20000) require.Error(t, err) LONG TEST - ZOOM IN
  43. func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10)

    require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) // More Setup _, err = fib.Fib(-5) require.Error(t, err) // More Setup _, err = fib.Fib(20000) require.Error(t, err) // More Setup _, err = fib.Fib(20001) require.Error(t, err) LONG TEST - ZOOM IN
  44. func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10)

    require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) // More Setup _, err = fib.Fib(-5) require.Error(t, err) // More Setup _, err = fib.Fib(20000) require.Error(t, err) // More Setup _, err = fib.Fib(20001) require.Error(t, err) // More Setup _, err = fib.Fib(20002) require.Error(t, err) // More Setup ... } LONG TEST - ZOOM IN
  45. func TestFibonaccierFib(t *testing.T) { // Setup ret, err := fib.Fib(10)

    require.NoError(t, err) require.Equal(t, *big.NewInt(55), ret) // More Setup _, err = fib.Fib(-5) require.Error(t, err) // More Setup _, err = fib.Fib(20000) require.Error(t, err) // More Setup _, err = fib.Fib(20001) require.Error(t, err) // More Setup _, err = fib.Fib(20002) require.Error(t, err) // More Setup ... } LONG TEST - ZOOM IN 500 LOC
  46. TABLE DRIVEN TESTS Each table entry is a complete test

    case with inputs and expected results ... Given a table of test cases, the actual test iterates through all table entries and for each entry performs the necessary tests. https://github.com/golang/go/wiki/TableDrivenTests
  47. TABLE DRIVEN TESTS go src/strings/compare_test.go var compareTests = []struct {

    a, b string i int }{ {"", "", 0}, {"a", "", 1}, {"", "a", -1}, {"abc", "abc", 0}, {"ab", "abc", -1}, {"abc", "ab", 1}, {"x", "ab", 1}, {"ab", "x", -1}, {"x", "a", 1}, {"b", "x", -1}, // test runtime·memeq's chunked implementation {"abcdefgh", "abcdefgh", 0}, {"abcdefghi", "abcdefghi", 0}, {"abcdefghi", "abcdefghj", -1}, } func TestCompare(t *testing.T) { for _, tt := range compareTests { cmp := Compare(tt.a, tt.b) if cmp != tt.i { t.Errorf(`Compare(%q, %q) = %v`, tt.a, tt.b, cmp) } } }
  48. TABLE DRIVEN TESTS func TestFibonaccierFib(t *testing.T) { // Setup ...

    for _, test := range []struct{ input int expected *big.Int } { { input: 10, expected: big.NewInt(55) }, { input:-5 }, { input:20000 }, } { if test.expected == nil { _, err := fib.Fib(test.input) require.Error(t, err) } else { mock.EXPECT().Fib(test.input).Times(1).Return(*test.expected, nil) out, err := fib.Fib(test.input) require.NoError(t, err) require.Equal(t, *test.expected, out) } } }
  49. TABLE DRIVEN TESTS func TestFibonaccierFib(t *testing.T) { // Setup ...

    for _, test := range []struct{ input int expected *big.Int } { { input: 10, expected: big.NewInt(55) }, { input:-5 }, { input:20000 }, } { if test.expected == nil { _, err := fib.Fib(test.input) require.Error(t, err) } else { mock.EXPECT().Fib(test.input).Times(1).Return(*test.expected, nil) out, err := fib.Fib(test.input) require.NoError(t, err) require.Equal(t, *test.expected, out) } } } $ go test ok github.com/twistlock/fibo/internal/server/server 0.002s
  50. CODE COVERAGE Test coverage is a term that describes how

    much of a package's code is exercised by running the package's tests. If executing the test suite causes 80% of the package's source statements to be run, we say that the test coverage is 80%. How it works? Rewrite the package's source code before compilation to add instrumentation, compile and run the modified source, and dump the statistics What is the impact? Its run-time overhead is therefore modest, adding only about 3% when running a typical (more realistic) test https://blog.golang.org/cover
  51. CODE COVERAGE func TestFibonaccierFib(t *testing.T) { // Setup ... for

    _, test := range []struct{ input int expected *big.Int } { { input: 10, expected: big.NewInt(55) }, { input:-5 }, { input:20000 }, } { if test.expected == nil { _, err := fib.Fib(test.input) require.Error(t, err) } else { mock.EXPECT().Fib(test.input).Times(1).Return(*test.expected, nil) out, err := fib.Fib(test.input) require.NoError(t, err) require.Equal(t, *test.expected, out) } } } $ go test --cover -coverprofile=coverage.out PASS coverage: 100.0% of statements ok
  52. CLI TEST func (c *cli) Run(n int) (out string, err

    error) { res, err := c.client.Fib(n) if err != nil { return "", err } var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 0, '\t', tabwriter.AlignRight) fmt.Fprintln(w, "n\tvalue") fmt.Fprintf(w, "%d\t%s", n, res.String()) w.Flush() return buf.String(), nil } package main type cli struct { client server.Fibonaccier }
  53. CLI TEST func (c *cli) Run(n int) (out string, err

    error) { res, err := c.client.Fib(n) if err != nil { return "", err } var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 0, '\t', tabwriter.AlignRight) fmt.Fprintln(w, "n\tvalue") fmt.Fprintf(w, "%d\t%s", n, res.String()) w.Flush() return buf.String(), nil } package main type cli struct { client server.Fibonaccier } func TestCliRun(t *testing.T) { // .... for _, test := range [] struct { n, v int out string } { {n: 10, v: 55, out: "n\tvalue\n10\t55"}, {n: 50, v: 12586269025, out:"n\tvalue\n50\t12586269025"}, } { mock.EXPECT().Fib(test.n).Return(*big.NewInt(int64(test.v)), nil) cli := cli{client:mock} data, err := cli.Run(test.n) require.NoError(t, err) require.Equal(t, test.out, string(data)) } }
  54. GOLDEN FILES Store the input and expected output in a

    separate file In GO, golden files usually go under testdata folder Great for testing long format string without polluting the code
  55. GOLDEN FILES $ docker cli $ ls -1a cli/command/container/testdata container-list-format-name-name.golden

    container-list-with-config-format.golden container-list-without-format.golden … 5
  56. GOLDEN FILES $ docker cli $ ls -1a cli/command/container/testdata container-list-format-name-name.golden

    container-list-with-config-format.golden container-list-without-format.golden … $ cat container-list-without-format.golden CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES container_id busybox:latest "top" Less than a second ago Up 1 second c1 container_id busybox:latest "top" Less than a second ago Up 1 second c2 container_id busybox:latest "top" Less than a second ago Up 1 second 80-82/tcp c3 ...
  57. CLI TEST $ go get gotest.tools/golden $ cat testdata/fib10.golden n

    value 10 555 func (c *cli) Run(n int) (out string, err error) { res, err := c.client.Fib(n) if err != nil { return "", err } var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 0, '\t', tabwriter.AlignRight) fmt.Fprintln(w, "n\tvalue") fmt.Fprintf(w, "%d\t%s", n, res.String()) w.Flush() return buf.String(), nil }
  58. CLI TEST $ go get gotest.tools/golden $ cat testdata/fib10.golden n

    value 10 555 func (c *cli) Run(n int) (out string, err error) { res, err := c.client.Fib(n) if err != nil { return "", err } var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 8, 0, '\t', tabwriter.AlignRight) fmt.Fprintln(w, "n\tvalue") fmt.Fprintf(w, "%d\t%s", n, res.String()) w.Flush() return buf.String(), nil } func TestCliRun(t *testing.T) { // .... for _, test := range [] struct { n, v int golden string } { {n: 10, v: 55, golden: "fib10.golden"}, {n: 50, v: 12586269025, golden:"fib55.golden"}, } { mock.EXPECT().Fib(test.n).Return(*big.NewInt(int64(test.v)), nil) cli := cli{client:mock} data, err := cli.Run(test.n) require.NoError(t, err) golden.Assert(t, data, test.golden) } }
  59. • Dependency injection pattern • Auto-generated mocks • Assertion library

    • Table driven tests • Incorporate code coverage in build • Use golden files SUMMARY