golang集成测试:dockertest testcontainers-go

2023-03-01 16:21:25 浏览数 (1)

在做集成测试的时候,每次测试前,如果通过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

0 人点赞