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

使用Testcontainers测试Doobie解释器时数据插入断言失败求助

问题分析与解决方案

我一眼就看出问题所在啦——你每次调用transactor.use的时候,都会重新创建一个全新的PostgreSQL容器和数据库连接池!

问题根源

你的transactor是一个Resource[IO, Transactor[IO]],而Resource的特性是每次调用use时,都会从头执行整个资源分配流程:

  1. 启动一个全新的PostgreSQL容器
  2. 创建新的连接池
  3. 执行你的数据库操作
  4. 关闭连接池并停止容器

这意味着:

  • 插入操作是在第一个临时容器里完成的,操作结束后容器就被销毁了
  • 查询操作又启动了第二个全新的容器,这个容器里根本没有你刚才插入的数据,自然返回None

解决方案:共享容器与连接池

我们需要让整个测试类共享同一个PostgreSQL容器和连接池,而不是每次操作都重新创建。这里用BeforeAndAfter来初始化和清理资源是最直接的方式:

class CompaniesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAndAfter with Matchers {
  // 声明共享的连接池和容器引用
  private var sharedXa: Transactor[IO] = _
  private var postgresContainer: PostgreSQLContainer[Nothing] = _

  // 测试前初始化一次资源
  before {
    val initResources = for {
      // 启动Postgres容器并执行初始化脚本
      container <- IO {
        val c = new PostgreSQLContainer().withInitScript("sql/companies.sql")
        c.start()
        c
      }
      // 创建固定大小的线程池
      ce <- ExecutionContexts.fixedThreadPool[IO](1)
      // 初始化连接池
      transactor <- HikariTransactor.newHikariTransactor[IO](
        "org.postgresql.Driver",
        container.getJdbcUrl,
        container.getUsername,
        container.getPassword,
        ce
      )
    } yield (container, transactor)

    // 同步执行初始化(因为before是同步方法)
    val (container, xa) = initResources.unsafeRunSync()
    postgresContainer = container
    sharedXa = xa
  }

  // 测试后清理资源
  after {
    // 关闭连接池和容器
    sharedXa.close().unsafeRunSync()
    postgresContainer.stop()
  }

  "Companies algebra " - {
    "should create a new Company" in {
      (for {
        // 使用共享的连接池执行插入
        companyId <- sql"INSERT INTO companies (name, description) VALUES ('My Company', 'My Company Description')"
          .update
          .withUniqueGeneratedKeys[Int]("id")
          .transact(sharedXa)
        // 用同一个连接池查询数据
        maybeCompany <- sql"SELECT * FROM companies WHERE id = $companyId"
          .query[Company]
          .option
          .transact(sharedXa)
      } yield maybeCompany)
        .asserting { company =>
          company shouldBe defined
          company.get.name shouldBe "My Company"
          company.get.description shouldBe "My Company Description"
        }
    }
  }
}

更优雅的优化建议

如果想更贴合Cats Effect的资源管理风格,可以使用ScalaTest的资源Fixture特性来管理整个测试的资源生命周期,避免手动处理unsafeRunSync():

import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.matchers.should.Matchers
import org.typelevel.cats.effect.testing.scalatest.AsyncIOSpec
import cats.effect.{IO, Resource}
import org.testcontainers.containers.PostgreSQLContainer

class CompaniesSpec extends AsyncFreeSpec with AsyncIOSpec with Matchers {

  // 定义共享资源:Postgres容器 + 连接池
  private val sharedResources: Resource[IO, Transactor[IO]] = for {
    container <- Resource.make(IO {
      val c = new PostgreSQLContainer().withInitScript("sql/companies.sql")
      c.start()
      c
    })(c => IO(c.stop()))
    ce <- ExecutionContexts.fixedThreadPool[IO](1)
    xa <- HikariTransactor.newHikariTransactor[IO](
      "org.postgresql.Driver",
      container.getJdbcUrl,
      container.getUsername,
      container.getPassword,
      ce
    )
  } yield xa

  // 使用Resource包裹测试,确保全程复用同一资源
  "Companies algebra " - {
    "should create a new Company" in sharedResources.use { xa =>
      (for {
        companyId <- sql"INSERT INTO companies (name, description) VALUES ('My Company', 'My Company Description')"
          .update
          .withUniqueGeneratedKeys[Int]("id")
          .transact(xa)
        maybeCompany <- sql"SELECT * FROM companies WHERE id = $companyId"
          .query[Company]
          .option
          .transact(xa)
      } yield maybeCompany)
        .asserting { company =>
          company shouldBe defined
          company.get.name shouldBe "My Company"
          company.get.description shouldBe "My Company Description"
        }
    }
  }
}

这种方式下,sharedResources只会初始化一次,整个测试都复用同一个容器和连接池,测试结束后自动释放资源,完全符合Cats Effect的资源管理理念。

内容的提问来源于stack exchange,提问作者riccardo.cardin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 19:37:48