在做集成测试的时候,每次测试前,如果通过docker重启一个干净的容器是不是免去了数据清理的苦恼。https://github.com/testcontainers/testcontainers-go和https://github.com/ory/dockertest可以解决我们的苦恼,它们很相似都是调用docker的api实现镜像的拉取和容器的启动关闭。然后我们可以基于容器做对应的集成测试。
由于每次拉取镜像和启动docker代价比较大,比较耗时,我们一般在单测的入口TestMain方法里做初始化,也就是一个模块进行一次容器初始化。由于单测case之间没有数据的清理,因此我们每个单测结束后都需要注意清理和还原数据。整体来说dockertest testcontainers-go 原理和使用方法比较类似。下面我们体验一下用法,首先我们需要启动docker
代码语言:javascript复制% docker version
Version: 20.10.12
dockertest
代码语言:javascript复制package dockertest_test
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
_ "github.com/go-sql-driver/mysql"
"github.com/ory/dockertest/v3"
)
var db *sql.DB
func TestMain(m *testing.M) {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not construct pool: %s", err)
}
// uses pool to try to connect to Docker
err = pool.Client.Ping()
if err != nil {
log.Fatalf("Could not connect to Docker: %s", err)
}
// pulls an image, creates a container based on it and runs it
resource, err := pool.Run("mysql", "5.7", []string{"MYSQL_ROOT_PASSWORD=secret"})
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
if err := pool.Retry(func() error {
var err error
db, err = sql.Open("mysql", fmt.Sprintf("root:secret@(localhost:%s)/mysql", resource.GetPort("3306/tcp")))
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to database: %s", err)
}
code := m.Run()
// You can't defer this because os.Exit doesn't care for defer
if err := pool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
os.Exit(code)
}
func TestSomething(t *testing.T) {
var CREATE_TABLE = "CREATE TABLE student("
"sid INT(10) NOT NULL AUTO_INCREMENT,"
"sname VARCHAR(64) NULL DEFAULT NULL,"
"age INT(10) DEFAULT NULL,PRIMARY KEY (sid))"
"ENGINE=InnoDB DEFAULT CHARSET=utf8;"
var INSERT_DATA = `INSERT INTO student(sid,sname,age) VALUES(?,?,?);`
var QUERY_DATA = `SELECT * FROM student;`
db.Query("create database test;")
db.Query("use test ;")
_, err := db.Exec(CREATE_TABLE)
fmt.Println("err")
db.Exec(INSERT_DATA, 1, "唐僧", 30)
// 查询数据
rows, err := db.Query(QUERY_DATA)
if err != nil {
fmt.Println(err)
}
for rows.Next() {
var name string
var id int
var age int
if err := rows.Scan(&id, &name, &age); err != nil {
fmt.Println(err)
}
fmt.Printf("%s is %dn", name, age)
}
}
testcontainers-go
代码语言:javascript复制package exp1
import (
"context"
"fmt"
"testing"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type redisContainer struct {
testcontainers.Container
URI string
}
func setupRedis(ctx context.Context) (*redisContainer, error) {
req := testcontainers.ContainerRequest{
Image: "redis:6",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("* Ready to accept connections"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}
mappedPort, err := container.MappedPort(ctx, "6379")
if err != nil {
return nil, err
}
hostIP, err := container.Host(ctx)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("redis://%s:%s", hostIP, mappedPort.Port())
return &redisContainer{Container: container, URI: uri}, nil
}
func TestIntegrationSetGet(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
ctx := context.Background()
redisContainer, err := setupRedis(ctx)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := redisContainer.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
})
// You will likely want to wrap your Redis package of choice in an
// interface to aid in unit testing and limit lock-in throughtout your
// codebase but that's out of scope for this example
options, err := redis.ParseURL(redisContainer.URI)
if err != nil {
t.Fatal(err)
}
client := redis.NewClient(options)
defer flushRedis(ctx, *client)
t.Log("pinging redis")
pong, err := client.Ping(ctx).Result()
require.NoError(t, err)
t.Log("received response from redis")
if pong != "PONG" {
t.Fatalf("received unexpected response from redis: %s", pong)
}
// Set data
key := fmt.Sprintf("{user.%s}.favoritefood", uuid.NewString())
value := "Cabbage Biscuits"
ttl, _ := time.ParseDuration("2h")
err = client.Set(ctx, key, value, ttl).Err()
if err != nil {
t.Fatal(err)
}
// Get data
savedValue, err := client.Get(ctx, key).Result()
if err != nil {
t.Fatal(err)
}
if savedValue != value {
t.Fatalf("Expected value %s. Got %s.", savedValue, value)
}
fmt.Println(key, savedValue)
}
func flushRedis(ctx context.Context, client redis.Client) error {
return client.FlushAll(ctx).Err()
}
两个包中的例子都列举了常用的中间件的用法,可以参考下
代码语言:javascript复制https://golang.testcontainers.org/examples/redis/
https://github.com/ory/dockertest/tree/v3/examples