TestContainer用户使用经验


TestContainer是集成测试的游戏规则改变者,它们具有特定于语言的 docker api,这使得启动容器并验证它们是否已完全初始化并准备好接受连接变得很简单。

不再需要模拟或复杂的环境配置。将测试依赖项定义为代码,然后只需运行测试,容器就会被创建然后删除。

不需要单元测试,用容器运行集成测试?

  • 至少在 Java 世界中,"单元测试 "一词经常被 "在 JUnit 中做的事情 "所混淆,JUnit 同时运行 "纯 "单元测试和项目级集成测试,即启动应用程序上下文(如 Spring)并针对真实 REST 端点等进行测试。
  • 虽然单元测试比(项目级)集成测试更便宜、更快捷,但在很多情况下,单元测试提供的结果和可信度也不如集成测试,因为很多运行时方面(序列化、HTTP 响应、数据库响应等)都无法直接模拟。
  • 使用实际数据库/Elasticsearch/Redis/Varnish 等的集成测试比传统的单元测试更有价值。

关于 "测试奖杯"(The Testing Trophy)而非 "测试金字塔"(Testing Pyramid)的讨论甚嚣尘上。集成测试的速度较慢,但也仅仅是慢了那么一点点,所以往往是值得的。是否值得在很大程度上取决于测试的内容。
  • 如果是 CRUD API:使用集成测试。
  • 如果是算法或字符串操作等:使用单元测试。


最佳实践
设置 CI,以便它进行 lint、构建、单元测试,然后集成测试(使用测试容器)
https://github.com/turbolytics/latte/blob/main/.github/workf...

name: CI
on: [push]
jobs:
  ci:
    name: CI
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go 1.21.x
        uses: actions/setup-go@v4
        with:
          go-version: 1.21.x
        id: go

      - name: Build
        run: go build -v ./...

      - name: Run unit tests
        run: make test-unit

      - name: Run integration tests
        run: make test-integration

它们的语言绑定为常见数据库操作提供了很好的辅助函数(例如从容器用户生成连接 uri)
https://github.com/turbolytics/latte/blob/main/internal/sour...

package mongodb

import (
    "context"
    
"fmt"
    
"github.com/stretchr/testify/assert"
    
"github.com/testcontainers/testcontainers-go"
    
"github.com/testcontainers/testcontainers-go/modules/mongodb"
    
"github.com/turbolytics/latte/internal/metric"
    
"go.mongodb.org/mongo-driver/mongo"
    
"go.mongodb.org/mongo-driver/mongo/options"
    
"testing"
    
"time"
)

func TestIntegration_Mongo_Source(t *testing.T) {
    if testing.Short() {
        t.Skip(
"skipping integration test")
    }

    ctx := context.Background()
    mongodbContainer, err := mongodb.RunContainer(
        ctx,
        testcontainers.WithImage(
"mongo:6"),
    )

    
// Clean up the container
    defer func() {
        if err := mongodbContainer.Terminate(ctx); err != nil {
            assert.NoError(t, err)
        }
    }()

    endpoint, err := mongodbContainer.ConnectionString(ctx)
    if err != nil {
        assert.NoError(t, err)
    }

    mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint))
    if err != nil {
        assert.NoError(t, err)
    }

    type user struct {
        Account    string
        SignupTime time.Time
    }

    coll := mongoClient.Database(
"test").Collection("users")
    docs := []interface{}{
        user{Account:
"amazon", SignupTime: time.Now().UTC()},
        user{Account:
"amazon", SignupTime: time.Now().UTC()},
        user{Account:
"google", SignupTime: time.Now().UTC()},
    }
    result, err := coll.InsertMany(context.TODO(), docs)
    fmt.Printf(
"Documents inserted: %v\n", len(result.InsertedIDs))

    mSource, err := NewFromGenericConfig(
        ctx,
        map[string]any{
            
"uri":        endpoint,
            
"database":   "test",
            
"collection": "users",
            
"agg": `
  [
    { 
      
"$group": { "_id": "$account", "value": { "$count": {} } } 
    },
    {
"$sort" : { "_id" : 1 } },
    { 
      
"$addFields": { 
        
"account": {  "$toString": "$_id"
      } 
    }, 
    { 
      
"$project": { "_id": 0 }
    }
  ]
`,
        },
        false,
    )

    assert.NoError(t, err)
    rs, err := mSource.Source(ctx)
    assert.NoError(t, err)

    ms := rs.(*metric.Metrics)

    for _, m := range ms.Metrics {
        m.Timestamp = time.Time{}
        m.UUID =
""
    }

    assert.Equal(t, []*metric.Metric{
        {
            Value: 2,
            Tags: map[string]string{
                
"account": "amazon",
            },
        },
        {
            Value: 1,
            Tags: map[string]string{
                
"account": "google",
            },
        },
    }, ms.Metrics)

}


可以在任何地方都使用它们:)

网友讨论:
1、作为 testcontainers 的用户,我可以告诉你它们非常强大但简单。

事实上,它们所做的只是为您的语言提供抽象,但这对于单元/集成测试非常有用。

在我的工作中,我们有许多 Java 和 Python 微服务,所有这些微服务都使用 testcontainers 来设置本地环境或集成测试。我发现与 localstack 的集成以及以编程方式设置它而无需与撰写文件进行斗争的能力非常有用。

2、我们使用 kubedock在 kubernetes 集群中运行测试容器。只要您只拉取图像,而不是构建或加载它们(kubedock 明确不支持),它就可以很好地工作。