You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Testcontainers测试PostgreSQL时遇索引越界运行时错误

修复Testcontainers测试PostgreSQL时的索引越界panic问题

问题分析

从panic栈信息来看,错误发生在MappedPort方法中,因容器端口映射列表为空导致索引越界。核心问题包括:

  • 使用的Testcontainers-go版本过旧(v0.7.0),存在端口映射相关bug
  • PostgreSQL连接字符串格式错误,占位符顺序不匹配
  • 等待策略仅监听端口,未确保PostgreSQL服务完全就绪
  • MappedPort调用时传入的端口格式不正确(缺少/tcp后缀)

修复步骤

  1. 升级Testcontainers-go版本
    将依赖升级到v0.20.x及以上稳定版本,修改go.mod:

    require github.com/testcontainers/testcontainers-go v0.20.0
    

    执行go mod tidy更新依赖。

  2. 修正连接字符串格式
    原连接字符串占位符顺序错误,数据库名应在端口之后,修改常量:

    const DBSource = "postgresql://%s:%s@%s:%d/%s?sslmode=disable"
    

    对应格式化代码调整为:

    source := fmt.Sprintf(DBSource, "postgres", "postgres", "localhost", port, "postgres")
    
  3. 完善容器等待策略
    仅监听端口不足以确保服务可用,添加pg_isready命令等待条件:

    WaitingFor: wait.ForAll(
        wait.ForListeningPort("5432/tcp"),
        wait.ForExec([]string{"pg_isready", "-U", "postgres"}).WithPollInterval(1*time.Second),
    ).WithDeadline(1*time.Minute),
    
  4. 修正MappedPort的端口参数
    Port方法中调用MappedPort时,传入完整的端口字符串:

    p, err := db.instance.MappedPort(ctx, "5432/tcp")
    
  5. 补全测试逻辑
    测试代码需调用待测试方法并断言结果,同时添加容器关闭逻辑:

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            tt.db = NewTestDatabase(t)
            defer tt.db.Close(t) // 测试结束后回收容器
    
            got, err := tt.db.SaveNote(tt.args.n)
            if (err != nil) != tt.wantErr {
                t.Errorf("SaveNote() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            // 忽略动态生成的Created字段直接对比,用实际返回值覆盖预期
            tt.want.Created = got.Created
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("SaveNote() = %v, want %v", got, tt.want)
            }
        })
    }
    

修改后的核心代码示例

NewTestDatabase函数

func NewTestDatabase(t *testing.T) *TestNoteEditor {
    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
    defer cancel()

    req := testcontainers.ContainerRequest{
        Image:        "postgres:12",
        ExposedPorts: []string{"5432/tcp"},
        AutoRemove:   true,
        Env: map[string]string{
            "POSTGRES_USER":     "postgres",
            "POSTGRES_PASSWORD": "postgres",
            "POSTGRES_DB":       "postgres",
        },
        WaitingFor: wait.ForAll(
            wait.ForListeningPort("5432/tcp"),
            wait.ForExec([]string{"pg_isready", "-U", "postgres"}).WithPollInterval(1*time.Second),
        ).WithDeadline(1*time.Minute),
    }

    postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatal("GenericContainer err = ", err)
    }

    require.NoError(t, err)

    testDb := TestDatabase{instance: postgresContainer}
    port := testDb.Port(t)
    source := fmt.Sprintf(DBSource, "postgres", "postgres", "localhost", port, "postgres")

    db, err := gorm.Open(postgres.Open(source), &gorm.Config{})
    if err != nil {
        t.Fatal("cannot create gorm: ", err)
    }

    // 自动迁移表结构(按需启用)
    err = db.AutoMigrate(&Note{})
    require.NoError(t, err)

    return &TestNoteEditor{
        db:       db,
        instance: postgresContainer,
    }
}

Port方法

func (db *TestDatabase) Port(t *testing.T) int {
    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
    defer cancel()
    p, err := db.instance.MappedPort(ctx, "5432/tcp")
    require.NoError(t, err)
    return p.Int()
}

注意事项

  • 确保Docker服务处于运行状态,Testcontainers依赖Docker环境
  • 测试结束后必须调用Close方法终止容器,避免资源泄漏
  • 对于动态生成的时间字段(如Created),断言时不要直接对比time.Now(),改用实际返回值或时间范围判断

内容的提问来源于stack exchange,提问作者pizhlo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 18:24:49