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.

C1508d6ff2ed86b2b4fedef9a3fe558f?s=128

GopherCon Israel

February 11, 2019
Tweet

Transcript

  1. The art of writing idiomatic test code Liron Levin, Twistlock

  2. Do you writing unit tests?

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

    • Regression • Easier to change code • Document and structure your API • Contributing to open source
  4. 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%
  5. STORY TIME

  6. STORY TIME We recently joined a company called complexify.io -

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

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

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

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

    ... } DONE
  11. SERVER package db func MkClient(connectionString string) (*Client, error) { //

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

    ... } package server func MkFibonaccier(connectionString string) (*fibonaccier, error) { dbClient, err := db.MkClient(connectionString) DONE
  13. 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
  14. 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
  15. 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
  16. 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
  17. SERVER

  18. SERVER DEPENDENCY INJECTION!

  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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 { ... }
  25. SERVER - INTERFACE INJECTION package db func MkClient(connectionString string) (*Client,

    error) { // Done! }
  26. SERVER - INTERFACE INJECTION package server type DBClient interface {

    Fib(n uint64) (big.Int, error) }
  27. 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) }
  28. 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) }
  29. SERVER - MOCKS

  30. SERVER - MOCKS MOCKS!

  31. MOCKS In short, mocking is creating objects that simulate the

    behavior of real objects. https://stackoverflow.com/questions/2665812/what-is-mocking
  32. 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
  33. 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 { ... }
  34. 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
  35. 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
  36. SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go

    -package=mock > server_mock.go
  37. 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) }
  38. 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 { ...
  39. 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) }
  40. 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()
  41. 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 })
  42. 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) /* ??? */ }
  43. SERVER - VALIDATION

  44. SERVER - VALIDATION USE T OR ASSERTION LIBRARY!

  45. 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) } ...
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. TABLE DRIVEN TESTS

  60. TABLE DRIVEN TESTS TABLE DRIVEN TESTS!

  61. 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
  62. 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) } } }
  63. 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) } } }
  64. 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
  65. 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
  66. 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
  67. CODE COVERAGE $ go tool cover -html=coverage.out

  68. CLI TEST $ Enter the fibonacci number: 10 n value

    10 55
  69. CLI TEST package main type cli struct { client server.Fibonaccier

    }
  70. 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 }
  71. 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)) } }
  72. GOLDEN FILES

  73. GOLDEN FILES GOLDEN FILES!

  74. 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
  75. 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
  76. 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 ...
  77. CLI TEST $ go get gotest.tools/golden $ cat testdata/fib10.golden n

    value 10 555
  78. 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 }
  79. 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) } }
  80. • Dependency injection pattern • Auto-generated mocks • Assertion library

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