Slide 1

Slide 1 text

The art of writing idiomatic test code Liron Levin, Twistlock

Slide 2

Slide 2 text

Do you writing unit tests?

Slide 3

Slide 3 text

Why should you write unit tests? ● Find bugs early ● Regression ● Easier to change code ● Document and structure your API ● Contributing to open source

Slide 4

Slide 4 text

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%

Slide 5

Slide 5 text

STORY TIME

Slide 6

Slide 6 text

STORY TIME We recently joined a company called complexify.io - a new, innovative company with the moto of making simple things complex.

Slide 7

Slide 7 text

STORY TIME As our first task, we were asked to build the world’s first fibonacci service. CLI Fibonacci service Fibonacci DB

Slide 8

Slide 8 text

STORY TIME As this is not a trivial project, we were assigned a supportive tech-lead

Slide 9

Slide 9 text

As this is not a trivial project, we were assigned a supportive tech-lead STORY TIME BOB!

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

SERVER

Slide 18

Slide 18 text

SERVER DEPENDENCY INJECTION!

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 { ... }

Slide 25

Slide 25 text

SERVER - INTERFACE INJECTION package db func MkClient(connectionString string) (*Client, error) { // Done! }

Slide 26

Slide 26 text

SERVER - INTERFACE INJECTION package server type DBClient interface { Fib(n uint64) (big.Int, error) }

Slide 27

Slide 27 text

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) }

Slide 28

Slide 28 text

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) }

Slide 29

Slide 29 text

SERVER - MOCKS

Slide 30

Slide 30 text

SERVER - MOCKS MOCKS!

Slide 31

Slide 31 text

MOCKS In short, mocking is creating objects that simulate the behavior of real objects. https://stackoverflow.com/questions/2665812/what-is-mocking

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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 { ... }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

SERVER - MOCKS $ go install github.com/golang/mock/mockgen $ mockgen -source=server.go -package=mock > server_mock.go

Slide 37

Slide 37 text

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) }

Slide 38

Slide 38 text

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 { ...

Slide 39

Slide 39 text

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) }

Slide 40

Slide 40 text

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()

Slide 41

Slide 41 text

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 })

Slide 42

Slide 42 text

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) /* ??? */ }

Slide 43

Slide 43 text

SERVER - VALIDATION

Slide 44

Slide 44 text

SERVER - VALIDATION USE T OR ASSERTION LIBRARY!

Slide 45

Slide 45 text

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) } ...

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

TABLE DRIVEN TESTS

Slide 60

Slide 60 text

TABLE DRIVEN TESTS TABLE DRIVEN TESTS!

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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) } } }

Slide 63

Slide 63 text

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) } } }

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

CODE COVERAGE $ go tool cover -html=coverage.out

Slide 68

Slide 68 text

CLI TEST $ Enter the fibonacci number: 10 n value 10 55

Slide 69

Slide 69 text

CLI TEST package main type cli struct { client server.Fibonaccier }

Slide 70

Slide 70 text

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 }

Slide 71

Slide 71 text

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)) } }

Slide 72

Slide 72 text

GOLDEN FILES

Slide 73

Slide 73 text

GOLDEN FILES GOLDEN FILES!

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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 ...

Slide 77

Slide 77 text

CLI TEST $ go get gotest.tools/golden $ cat testdata/fib10.golden n value 10 555

Slide 78

Slide 78 text

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 }

Slide 79

Slide 79 text

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) } }

Slide 80

Slide 80 text

● Dependency injection pattern ● Auto-generated mocks ● Assertion library ● Table driven tests ● Incorporate code coverage in build ● Use golden files SUMMARY