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

談談 Golang 測試的二三事

談談 Golang 測試的二三事

David Chou

July 26, 2020
Tweet

More Decks by David Chou

Other Decks in Programming

Transcript

  1. @ Umbo Computer Vision @ Golang Taipei david74.chou @ facebook

    david74.chou @ medium david7482 @ github Golang Taipei Telegram Golang Taipei Facebook
  2. go test go test -v -run={TestName} go test -race go

    test -count=1 go test -c -o {TestExe} go test -coverprofile=coverage.out go tool cover -html=coverage.out
  3. My make test $ go test -race -cover -coverprofile cover.out

    $ go tool cover -func=cover.out | tail -n 1 | awk '{print $3}' $ go test -race -coverprofile cover.out ok github.com/.../internal/pkg/apiutil 0.334s coverage: 53.3% of sta ok github.com/.../internal/pkg/emailutil 17.611s coverage: 90.7% of sta ok github.com/.../internal/pkg/integrationutil 2.026s coverage: 0.0% ok github.com/.../internal/pkg/lambdautil 0.829s coverage: 62.5% of sta ok github.com/.../internal/pkg/s3util 2.056s coverage: 81.8% of sta ok github.com/.../internal/pkg/smtp/processor/attachment 0.627s covera ok github.com/.../internal/pkg/smtp/processor/event 0.082s coverage: 71.4 ok github.com/.../internal/pkg/smtp/processor/integration 0.060s covera ok github.com/.../internal/pkg/smtp/processor/lambda 0.415s coverage: 82.4 ok github.com/.../internal/pkg/smtp/processor/payload 0.053s covera ok github.com/.../internal/pkg/videoutil 0.413s coverage: 75.9% of sta $ go tool cover -func=cover.out | tail -n 1 57.8% 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  4. func TestSomething(t *testing.T) { value, err := DoSomething() if err

    != nil { t.Fatalf("DoSomething() failed: } if value != 100 { t.Fatalf("expected 100, got: %d" } } 1 2 3 4 5 6 7 8 9 import "github.com/stretchr/testify/asse func TestSomething(t *testing.T) { value, err := DoSomething() assert.NoError(t, err) assert.EqualValues(t, 100, value) } 1 2 3 4 5 6 7 testify
  5. import "github.com/stretchr/testify/asse assert.NoError(t, err) assert.EqualValues(t, 100, value) 1 2 func

    TestSomething(t *testing.T) { 3 value, err := DoSomething() 4 5 6 } 7 import "github.com/stretchr/testify/requ require.NoError(t, err) require.EqualValues(t, 100, value) 1 2 func TestSomething(t *testing.T) { 3 value, err := DoSomething() 4 5 6 } 7 testify require should be better in most cases
  6. Test helper functions func testTempFile(t *testing.T) (string, func()) f, err

    := ioutil.TempFile("", "test") require.NoError(t, err) f.Close() return f.Name(), func() { os.Remove(f.Na } func TestThing(t *testing.T) { f, remove := testTempFile(t) defer remove() } 1 2 3 4 5 6 7 8 9 10 11 Advanced testing in Go
  7. Test helper functions func testChdir(t *testing.T, dir string) func() pwd,

    err := os.Getwd() require.NoError(t, err) err = os.Chdir(dir) require.NoError(t, err) return func() { os.Chdir(pwd) } } func TestThing(t *testing.T) { defer testChdir(t, "/other")() } 1 2 3 4 5 6 7 8 9 10 11 12 13 Advanced testing in Go
  8. Test helper functions Advanced testing in Go Don't return error,

    but pass in testing.T and fail No error return; the usage could be quite clean Return a func() to clean up resource or recover state The func() can access testing.T to also fail
  9. func IsStrExist(slice []string, find str for _, v := range

    slice { if v == find { return true } } return false } 1 2 3 4 5 6 7 8 func TestIsStrExistSimple(t *testing.T) ok := IsStrExist([]string{"a", require.True(t, ok) ok = IsStrExist([]string{"a", " require.False(t, ok) ok = IsStrExist([]string{}, "a" require.False(t, ok) ok = IsStrExist(nil, "a") require.False(t, ok) } 1 2 3 4 5 6 7 8 9 10 11 12 13 Table-driven test
  10. func TestIsStrExist(t *testing.T) { tests := map[string]struct { slice []string

    find string want bool }{ "find": {[]string{"a", "b"}, "a", "notfind": {[]string{"a", "b"}, "c", fa "empty": {[]string{}, "a", false}, "nil": {nil, "a", false}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { got := IsStrExist(tt.slice, tt.find) require.EqualValues(t, tt.want, }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Table-driven test
  11. func TestIsStrExistP(t *testing.T) { tests := map[string]struct { slice []string

    find string want bool }{ "find": {[]string{"a", "b"}, "a", true}, "notfind": {[]string{"a", "b"}, "c", false}, "empty": {[]string{}, "a", false}, "nil": {nil, "a", false}, } for name, tt := range tests { tt := tt // fix loop iterator variable issue t.Run(name, func(t *testing.T) { t.Parallel() require.EqualValues(t, tt.want, IsStrExist(tt.sl }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 tt := tt // fix loop iterator variable issue t.Parallel() func TestIsStrExistP(t *testing.T) { 1 tests := map[string]struct { 2 slice []string 3 find string 4 want bool 5 }{ 6 "find": {[]string{"a", "b"}, "a", true}, 7 "notfind": {[]string{"a", "b"}, "c", false}, 8 "empty": {[]string{}, "a", false}, 9 "nil": {nil, "a", false}, 10 } 11 for name, tt := range tests { 12 13 t.Run(name, func(t *testing.T) { 14 15 require.EqualValues(t, tt.want, IsStrExist(tt.sl 16 }) 17 } 18 } 19 Table-driven test in parallel
  12. Table-driven test Easier to add new test cases Easier to

    exhaustive corner cases Make reproducing issues simple Try to do this pattern if possible
  13. “ that code is testable when we don’t have to

    change the code itself when we’re adding a unit test to it
  14. Accept interface, return concrete type concrete type is usually pointer

    or struct Do not define interfaces on the implementor side Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values.
  15. package s3 type S3 struct {...} func New(...) *S3 {...}

    func (s *S3) CopyObject(*s3.CopyObjectIn func (s *S3) CopyObjectWithContext(aws.C func (s *S3) CopyObjectRequest(*s3.CopyO func (s *S3) GetObject(*s3.GetObjectInpu func (s *S3) GetObjectWithContext(aws.Co func (s *S3) GetObjectRequest(*s3.GetObj ---- type S3API interface { CopyObject(*s3.CopyObjectInput) CopyObjectWithContext(aws.Contex CopyObjectRequest(*s3.CopyObject GetObject(*s3.GetObjectInput) (* GetObjectWithContext(aws.Context GetObjectRequest(*s3.GetObjectIn // ... over 300 functions } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package s3client type S3Service interface { GetObject(*s3.GetObjectInput) (*s3.GetObjec } func Download(s3Srv S3Service, bucket, key string) func Main() { s3Srv := s3.New() err := Download(s3Srv, "bucket", "key") ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 package s3client_test type S3ServiceMock struct {} func (m S3ServiceMock) GetObject(*s3.GetObjectInput func TestDownload(t *testing.T) { var m S3ServiceMock err := Download(m, "mybucket", "mykey") ... } 1 2 3 4 5 6 7 8 9 10 11
  16. type OrderModel interface { GetOrderByID(orderID string) (Order, error) } type

    PaymentModel interface { GetPaymentByID(paymentID string) (Payment, error) } func IsOrderPaid(orderModel OrderModel, paymentModel PaymentModel, orderID string) (ok b var order Order if order, err = orderModel.GetOrderByID(orderID); err != nil { return false, err } var payment Payment if payment, err = paymentModel.GetPaymentByID(order.paymentID); err != nil { return false, err } return payment.status == "completed", nil } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 IsOrderPaid()
  17. type OrderModelMock struct{} func (m OrderModelMock) GetOrderByID(orderID string) (Order, error)

    { ... } type PaymentModelMock struct{} func (m PaymentModelMock) GetPaymentByID(paymentID string) (Payment, error) { func TestIsOrderPaidSimple(t *testing.T) { var orderMock OrderModelMock var paymentMock PaymentModelMock ok, err := IsOrderPaid(orderMock, paymentMock, "orderId") ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Too boilerplate to write all these mockers Hard to add different behaviors into mockers Need manually update mockers if interface changes
  18. gomock //go:generate mockgen -destination automocks/ordermodel.go -package=automocks . Orde type OrderModel

    interface { GetOrderByID(orderID string) (Order, error) } //go:generate mockgen -destination automocks/paymentmodel.go -package=automocks . Pa type PaymentModel interface { GetPaymentByID(paymentID string) (Payment, error) } 1 2 3 4 5 6 7 8 9 $ go generate -x ./... mockgen -destination automocks/ordermodel.go -package=automocks . OrderModel mockgen -destination automocks/paymentmodel.go -package=automocks . PaymentModel 1 2 3 run go generate to generate mocking objects
  19. gomock m := automocks.NewMockOrderModel(ctrl) m.EXPECT(). GetOrderByID(gomock.Any()). Return(Order{}, nil). Times(1) m.EXPECT().

    GetOrderByID("correctOrderID"). DoAndReturn(func(orderID string) (Order ... }).MinTimes(3) 1 2 3 4 5 6 7 8 9 10 11 12 m := automocks.NewMockOrderModel(ctrl) 1 m.EXPECT(). 2 GetOrderByID(gomock.Any()). 3 Return(Order{}, nil). 4 Times(1) 5 6 7 m.EXPECT(). 8 GetOrderByID("correctOrderID"). 9 DoAndReturn(func(orderID string) (Order 10 ... 11 }).MinTimes(3) 12 m.EXPECT(). GetOrderByID(gomock.Any()). Return(Order{}, nil). Times(1) m := automocks.NewMockOrderModel(ctrl) 1 2 3 4 5 6 7 m.EXPECT(). 8 GetOrderByID("correctOrderID"). 9 DoAndReturn(func(orderID string) (Order 10 ... 11 }).MinTimes(3) 12 m.EXPECT(). GetOrderByID("correctOrderID"). DoAndReturn(func(orderID string) (Order ... }).MinTimes(3) m := automocks.NewMockOrderModel(ctrl) 1 m.EXPECT(). 2 GetOrderByID(gomock.Any()). 3 Return(Order{}, nil). 4 Times(1) 5 6 7 8 9 10 11 12 create mocker with expectation API
  20. func TestIsOrderPaid(t *testing.T) { tests := []struct { name string

    args args wantOk bool wantErr error }{ { name: "paid", args: args{...}, wantOk: true, wantErr: nil, }, { name: "notpaid", args: args{...}, wantOk: false, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() orderModel := getOrderModel(t, ctrl, tt.arg paymentModel := getPaymentModel(t, ctrl, tt gotOk, err := IsOrderPaid(orderModel, payme ... }) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 args args func TestIsOrderPaid(t *testing.T) { 1 tests := []struct { 2 name string 3 4 wantOk bool 5 wantErr error 6 }{ 7 { 8 name: "paid", 9 args: args{...}, 10 wantOk: true, 11 wantErr: nil, 12 }, 13 { 14 name: "notpaid", 15 args: args{...}, 16 wantOk: false, 17 wantErr: nil, 18 }, 19 } 20 for _, tt := range tests { 21 t.Run(tt.name, func(t *testing.T) { 22 ctrl := gomock.NewController(t) 23 defer ctrl.Finish() 24 25 orderModel := getOrderModel(t, ctrl, tt.arg 26 paymentModel := getPaymentModel(t, ctrl, tt 27 28 gotOk, err := IsOrderPaid(orderModel, payme 29 ... 30 }) 31 } 32 } 33 { name: "paid", args: args{...}, wantOk: true, wantErr: nil, }, { name: "notpaid", args: args{...}, wantOk: false, wantErr: nil, }, func TestIsOrderPaid(t *testing.T) { 1 tests := []struct { 2 name string 3 args args 4 wantOk bool 5 wantErr error 6 }{ 7 8 9 10 11 12 13 14 15 16 17 18 19 } 20 for _, tt := range tests { 21 t.Run(tt.name, func(t *testing.T) { 22 ctrl := gomock.NewController(t) 23 defer ctrl.Finish() 24 25 orderModel := getOrderModel(t, ctrl, tt.arg 26 paymentModel := getPaymentModel(t, ctrl, tt 27 28 gotOk, err := IsOrderPaid(orderModel, payme 29 ... 30 }) 31 } 32 } 33 ctrl := gomock.NewController(t) defer ctrl.Finish() func TestIsOrderPaid(t *testing.T) { 1 tests := []struct { 2 name string 3 args args 4 wantOk bool 5 wantErr error 6 }{ 7 { 8 name: "paid", 9 args: args{...}, 10 wantOk: true, 11 wantErr: nil, 12 }, 13 { 14 name: "notpaid", 15 args: args{...}, 16 wantOk: false, 17 wantErr: nil, 18 }, 19 } 20 for _, tt := range tests { 21 t.Run(tt.name, func(t *testing.T) { 22 23 24 25 orderModel := getOrderModel(t, ctrl, tt.arg 26 paymentModel := getPaymentModel(t, ctrl, tt 27 28 gotOk, err := IsOrderPaid(orderModel, payme 29 ... 30 }) 31 } 32 } 33 orderModel := getOrderModel(t, ctrl, tt.arg paymentModel := getPaymentModel(t, ctrl, tt gotOk, err := IsOrderPaid(orderModel, payme func TestIsOrderPaid(t *testing.T) { 1 tests := []struct { 2 name string 3 args args 4 wantOk bool 5 wantErr error 6 }{ 7 { 8 name: "paid", 9 args: args{...}, 10 wantOk: true, 11 wantErr: nil, 12 }, 13 { 14 name: "notpaid", 15 args: args{...}, 16 wantOk: false, 17 wantErr: nil, 18 }, 19 } 20 for _, tt := range tests { 21 t.Run(tt.name, func(t *testing.T) { 22 ctrl := gomock.NewController(t) 23 defer ctrl.Finish() 24 25 26 27 28 29 ... 30 }) 31 } 32 } 33 type args struct { orderID string order Order orderErr error paymentID string payment Payment paymentErr error } 1 2 3 4 5 6 7 8 9 func getOrderModel(c *gomock.Controller, args args) Or orderModel := automocks.NewMockOrderModel(c) orderModel.EXPECT(). GetOrderByID(args.orderID). Return(args.order, args.orderErr). Times(1) return orderModel } 1 2 3 4 5 6 7 8
  21. func LaunchDB(t *testing.T) (*sql.DB, func()) { pool, _ := dockertest.NewPool("")

    // pulls an image, creates a container based on resource, _ := pool.RunWithOptions(&dockertest. Repository: "mysql", Tag: "8.0", Env: []string{"MYSQL_ROOT_PASSWO }) // exponential backoff-retry var db *sql.DB pool.Retry(func() error { var err error db, err = sql.Open("mysql", DSN) if err != nil { return err } return db.Ping() }) return db, func() { pool.Purge(resource) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var db *sql.DB func TestMain(m *testing.M) { var close func() db, close = testutil.Launch // Run all integration test code := m.Run() // Close db container close() os.Exit(code) } func TestSomething(t *testing.T) { // db.Query() } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
  22. ory/dockertest Launch container just in your go test code Remove

    containers just after test finishes Smoothly integrate with go test framework and your IDE Could use build-tag to run integration test explicitly // +build integration import testing 1 2 3 $ go test --tags integration ./...