David Chou @ Crescendo Lab
CC-BY-SA-3.0-TW
$ go test -race -coverprofile cover.out ok github.com/.../internal/pkg/apiutil 0.334s coverage: 53.3% of statements ok github.com/.../internal/pkg/emailutil 17.611s coverage: 90.7% of statements ok github.com/.../internal/pkg/integrationutil 2.026s coverage: 0.0% of statements ok github.com/.../internal/pkg/lambdautil 0.829s coverage: 62.5% of statements ok github.com/.../internal/pkg/s3util 2.056s coverage: 81.8% of statements ok github.com/.../internal/pkg/smtp/processor/attachment 0.627s coverage: 76.4% of statements ok github.com/.../internal/pkg/smtp/processor/event 0.082s coverage: 71.4% of statements ok github.com/.../internal/pkg/smtp/processor/integration 0.060s coverage: 15.4% of statements ok github.com/.../internal/pkg/smtp/processor/lambda 0.415s coverage: 82.4% of statements ok github.com/.../internal/pkg/smtp/processor/payload 0.053s coverage: 68.8% of statements ok github.com/.../internal/pkg/videoutil 0.413s coverage: 75.9% of statements $ go tool cover -func=cover.out | tail -n 1 57.8%
func TestSomething(t *testing.T) { value, err := DoSomething() if err != nil { t.Fatalf("DoSomething() failed: %s", err) } if value != 100 { t.Fatalf("expected 100, got: %d", value) } }
import "github.com/stretchr/testify/assert" func TestSomething(t *testing.T) { value, err := DoSomething() assert.NoError(t, err) assert.EqualValues(t, 100, value) }
func TestEventuallyExample(t *testing.T) { status := "pending" // 模擬一個異步操作,1 秒後將 status 更新為 "done" go func() { time.Sleep(1 * time.Second) status = "done" }() // 使用 assert.Eventually 確保 status 在 2 秒內變為 "done" assert.Eventually(t, func() bool { return status == "done" }, 2*time.Second, 100*time.Millisecond, "status should eventually be 'done'") }
func (suite *MyDBTestSuite) TestCreateUser() { suite.NoError(createUser()) } func (suite *MyDBTestSuite) TestDeleteUser() { suite.NoError(deleteUser()) }
type MyDBTestSuite struct { suite.Suite } func (suite *MyDBTestSuite) SetupSuite() { // run db migrations, setup all tables } func (suite *MyDBTestSuite) TearDownSuite() { // do nothing } func (suite *MyDBTestSuite) SetupTest() { // insert mock data } func (suite *MyDBTestSuite) TearDownTest() { // remove all data }
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.Name()) } } func TestThing(t *testing.T) { f, remove := testTempFile(t) defer remove() // do something with the temp file }
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.Name()) } } func TestThing(t *testing.T) { f, remove := testTempFile(t) defer remove() // do something with the temp file }
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.Name()) } } func TestThing(t *testing.T) { f, remove := testTempFile(t) defer remove() // do something with the temp file }
func IsStrExist(slice []string, find string) bool { for _, v := range slice { if v == find { return true } } return false }
func TestIsStrExistSimple(t *testing.T) { ok := IsStrExist([]string{"a", "b"}, "a") require.True(t, ok) ok = IsStrExist([]string{"a", "b"}, "c") require.False(t, ok) ok = IsStrExist([]string{}, "a") require.False(t, ok) ok = IsStrExist(nil, "a") require.False(t, ok) }
func TestIsStrExist(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 { t.Run(name, func(t *testing.T) { got := IsStrExist(tt.slice, tt.find) require.EqualValues(t, tt.want, got) }) } }
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; no need if >= 1.22 t.Run(name, func(t *testing.T) { t.Parallel() require.EqualValues(t, tt.want, IsStrExist(tt.slice, tt.find)) }) } }
that code is testable when we don’t have to change the code itself when we’re adding a unit test to it
concrete type is usually pointer or struct
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values.
package s3 type S3 struct {...} func New(...) *S3 {...} func (s *S3) CopyObject(*s3.CopyObjectInput) (*s3.CopyObjectOutput, error) func (s *S3) CopyObjectWithContext(aws.Context, *s3.CopyObjectInput, ...request.Option) (*s3.CopyObjectOutput, error) func (s *S3) CopyObjectRequest(*s3.CopyObjectInput) (*request.Request, *s3.CopyObjectOutput) func (s *S3) GetObject(*s3.GetObjectInput) (*s3.GetObjectOutput, error) func (s *S3) GetObjectWithContext(aws.Context, *s3.GetObjectInput, ...request.Option) (*s3.GetObjectOutput, error) func (s *S3) GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) ---- type S3API interface { CopyObject(*s3.CopyObjectInput) (*s3.CopyObjectOutput, error) CopyObjectWithContext(aws.Context, *s3.CopyObjectInput, ...request.Option) (*s3.CopyObjectOutput, error) CopyObjectRequest(*s3.CopyObjectInput) (*request.Request, *s3.CopyObjectOutput) GetObject(*s3.GetObjectInput) (*s3.GetObjectOutput, error) GetObjectWithContext(aws.Context, *s3.GetObjectInput, ...request.Option) (*s3.GetObjectOutput, error) GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) // ... over 300 functions }
package s3client type S3Service interface { GetObject(*s3.GetObjectInput) (*s3.GetObjectOutput, error) } func Download(s3Srv S3Service, bucket, key string) error {...} func Main() { s3Srv := s3.New() err := Download(s3Srv, "bucket", "key") ... }
package s3client_test type S3ServiceMock struct {} func (m S3ServiceMock) GetObject(*s3.GetObjectInput) (*s3.GetObjectOutput, error) {...} func TestDownload(t *testing.T) { var m S3ServiceMock err := Download(m, "mybucket", "mykey") ... }
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 bool, err error) { 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 }
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") ... }
//go:generate mockgen -destination automocks/ordermodel.go -package=automocks . OrderModel type OrderModel interface { GetOrderByID(orderID string) (Order, error) } //go:generate mockgen -destination automocks/paymentmodel.go -package=automocks . PaymentModel type PaymentModel interface { GetPaymentByID(paymentID string) (Payment, error) }
$ go generate -x ./...
mockgen -destination automocks/ordermodel.go -package=automocks . OrderModel
mockgen -destination automocks/paymentmodel.go -package=automocks . PaymentModelm := automocks.NewMockOrderModel(ctrl) m.EXPECT(). GetOrderByID(gomock.Any()). Return(Order{}, nil). Times(1) m.EXPECT(). GetOrderByID("correctOrderID"). DoAndReturn(func(orderID string) (Order, error) { ... }).MinTimes(3)
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.args) paymentModel := getPaymentModel(t, ctrl, tt.args) gotOk, err := IsOrderPaid(orderModel, paymentModel, tt.args.orderID) ... }) } }
type args struct { orderID string order Order orderErr error paymentID string payment Payment paymentErr error }
func getOrderModel(c *gomock.Controller, args args) OrderModel { orderModel := automocks.NewMockOrderModel(c) orderModel.EXPECT(). GetOrderByID(args.orderID). Return(args.order, args.orderErr). Times(1) return orderModel }
testAnswer := "test123"
testResult := 1024
Generate fake data with no effort
https://github.com/go-faker/faker
type AStruct struct { Number int64 Height int64 B BStruct } type BStruct struct { Image string } func Example_withoutTag() { a := AStruct{} faker.FakeData(&a) fmt.Printf("%+v", a) /* Result: { Number: 1 Height: 26 B: { Image: RmmaaHkAkrWHldVbBNuQSKlRb } } */ }
faker.Email() // => mJBJtbv@OSAaT.com faker.URL() // => https://www.oEuqqAY.org/QgqfOhd faker.FirstName() // => Whitney faker.E164PhoneNumber() // => +724891571063 faker.UUIDHyphenated() // => 8f8e4463-9560-4a38-9b0c-ef24481e4e27 faker.IPv4() // => 99.23.42.63 faker.IPv6() // => 975c:fb2c:2133:fbdd:beda:282e:1e0a:ec7d faker.Word()) // => nesciunt faker.Sentence() // => Consequatur perferendis voluptatem accusantium. faker.TimeString() // => 03:10:25 faker.MonthName() // => February faker.YearString() // => 1994 faker.DayOfWeek() // => Sunday faker.DayOfMonth() // => 20 faker.Timestamp() // => 1973-06-21 14:50:46 ...
// For deterministic result
faker.SetRandomSource(faker.NewSafeSource(rand.NewSource(seed)))Insert predefined data into database
https://github.com/go-testfixtures/testfixtures
# comments.yml - id: 1 post_id: 1 content: A comment... author_name: John Doe author_email: john@doe.com created_at: 2020-12-31 23:59:59 updated_at: 2020-12-31 23:59:59 - id: 2 post_id: 2 content: Another comment... author_name: John Doe author_email: john@doe.com created_at: 2020-12-31 23:59:59 updated_at: 2020-12-31 23:59:59
loader, err := testfixtures.New( testfixtures.Database(db.DB), testfixtures.Dialect("postgres"), testfixtures.UseAlterConstraint(), testfixtures.Location(time.UTC), // Load predefined data testfixtures.Files( "fixtures/orders.yml", "fixtures/customers.yml", ), ) loader.Load()
func LaunchDB(t *testing.T) (*sql.DB, func()) { pool, _ := dockertest.NewPool("") // pulls an image, creates a container based on it and runs it resource, _ := pool.RunWithOptions(&dockertest.RunOptions{ Repository: "mysql", Tag: "8.0", Env: []string{"MYSQL_ROOT_PASSWORD=secret"}, }) // 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) } }
var db *sql.DB func TestMain(m *testing.M) { var close func() db, close = testutil.LaunchDB() // Run all integration tests code := m.Run() // Close db container close() os.Exit(code) } func TestSomething(t *testing.T) { // db.Query() }
// +build integration
import testing$ go test --tags integration ./...