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)
}
func IsOrderPaid(orderModel OrderModel, orderID string) (ok bool, err error) {
var order Order
if order, err = orderModel.GetOrderByID(orderID); err != nil {
return false, err
}
return order.status == "completed", nil
}type OrderModelMock struct{}
func (m OrderModelMock) GetOrderByID(orderID string) (Order, error) { ... }
func TestIsOrderPaidSimple(t *testing.T) {
var orderMock OrderModelMock
ok, err := IsOrderPaid(orderMock, "orderId")
...
}//go:generate mockgen -destination automocks/ordermodel.go -package=automocks . OrderModel
type OrderModel interface {
GetOrderByID(orderID string) (Order, error)
}$ go generate -x ./...
mockgen -destination automocks/ordermodel.go -package=automocks . OrderModelm := 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)
gotOk, err := IsOrderPaid(orderModel, tt.args.orderID)
...
})
}
}type args struct {
orderID string
order Order
orderErr 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:59loader, err := testfixtures.New(
testfixtures.Database(db.DB),
testfixtures.Dialect("postgres"),
testfixtures.Location(time.UTC),
// Load predefined data
testfixtures.Files(
"fixtures/users.yml",
"fixtures/comments.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 ./...