Testcontainers 入门:用真实服务容器解决集成测试的环境与数据污染
文章是 Testcontainers 官方入门指南,先梳理传统集成测试的两类痛点:预置环境难以维护、并行管道互相污染。作者指出,用 H2 这类内存库或 mock 替代真实服务会丢失生产特性并延迟反馈,SQL 可能在部署后才暴露兼容问题,从而让测试失去意义。随后介绍 Testcontainers 的工作方式:在测试前后用 Docker-API 兼容运行时拉起和销毁真实数据库、消息系统等容器,基础设施定义与测试代码放在一起,可在 IDE 直接运行,也天然隔离并行执行。全文适合刚接触集成测试、想绕开 mock 替代方案的工程师;但没有任何代码示例和性能数据,也未提及 Testcontainers 自身的 Docker 依赖与启动开销。
Modern software systems tackle complex business problems by leveraging various technologies and tools. Nowadays, hardly any software system works in isolation; they usually talk to databases, messaging systems, and cache providers and interact with many other 3rd party services. In today’s highly competitive market, time-to-market is crucial. Businesses want to put their product on the market as soon as possible, get feedback and iterate on it. To achieve that aspect of agility, one should have a solid Continuous Integration and Continuous Deployment (CI/CD) process. A crucial part of the CI/CD process is automated testing to ensure the correctness of the application behavior.
现代软件系统往往借助各种技术和工具来解决复杂的业务问题。如今,几乎没有任何软件系统是孤立工作的;它们通常要连接数据库、消息系统、缓存服务,并与其他许多第三方服务交互。在当今竞争激烈的市场中,产品上市时间至关重要。企业希望尽快把产品推向市场,获取反馈并不断迭代。要实现这种敏捷性,就必须拥有扎实的持续集成与持续交付(CI/CD)流程。而 CI/CD 流程中的关键一环,就是用自动化测试来保证应用行为的正确性。
While Unit Testing helps in testing the business logic and implementation details by isolating from external services like databases, messaging systems etc., the bulk of the application code might still be in integrating with those external services. To be fully confident with our application, we should write integration tests along with unit tests to ensure that our application is fully functional.
虽然单元测试通过隔离数据库、消息系统等外部服务,有助于测试业务逻辑和实现细节,但应用程序代码的大部分工作可能仍然集中在与这些外部服务的集成上。为了对我们的应用有充分的信心,除了单元测试之外,我们还应该编写集成测试,以确保应用功能完善。
Historically Integration testing is considered difficult because of the challenges in maintaining an "integration testing environment". Integration testing with pre-provisioned infrastructure is challenging because of the following reasons:
Before running tests, you must ensure that the infrastructure is up and running and data is pre-configured in a specific desired state.
If multiple build pipelines run in parallel, then one test execution might interfere with other test data, therefore resulting in flaky tests or other issues of test pollution.
从历史上看,集成测试之所以被认为困难,是因为维护“集成测试环境”本身就充满挑战。使用预先配置好的基础设施进行集成测试,会遇到以下问题:
在运行测试之前,你必须确保基础设施已启动并运行,并且数据已按照特定的预期状态预先配置好。
如果有多个构建管道并行运行,那么一次测试执行可能会干扰其他测试数据,从而导致测试不稳定或出现测试污染等问题。
Due to the challenges mentioned above, some people lean towards using services with in-memory or embedded variations of the required services for integration testing. For example, if an application uses the Postgres database, then H2 in-memory database is used as a substitute for testing. While this is an improvement over not writing integration tests at all, using mocks or in-memory versions of those services brings its own problems:
In-memory services may not have all the features of your production service. For example, you might be using advanced features of Postgres/Oracle databases in your application. But H2 might not support all those features in order to use it for integration testing. In the worst case, this might even lead to developers being cautious of adopting powerful features of those systems because of issues with replicating this functionality with the corresponding substitutes.
由于上述挑战,一些人倾向于在集成测试中使用所需服务的内存版或嵌入式变体。例如,如果应用使用 Postgres 数据库,就会用 H2 内存数据库替代进行测试。虽然这比完全不写集成测试要好,但使用 mock 或这些服务的内存版也会带来自己的问题:
内存服务可能并不具备生产服务的全部特性。例如,你的应用可能会用到 Postgres/Oracle 数据库的高级特性,但 H2 可能并不支持这些特性,因而无法用于集成测试。最糟糕的情况下,这甚至会让开发者在采用这些系统强大的功能时变得谨慎,因为用相应的替代品很难复现这些功能。
In-memory services delay the feedback cycle. For example, you might have written an SQL query and tested it with an H2 in-memory database which is working fine. But after deploying the application, you may realize the query syntax works fine for H2 but not with your production database Postgres/Oracle. Or maybe you have to maintain multiple different implementations to mitigate this issue? This kind of testing effectively defeats the purpose of testing, which is to get faster feedback cycles on my changes.
内存服务会延迟反馈周期。例如,你可能写了一条 SQL 查询,在 H2 内存数据库上测试一切正常。但应用部署后,你可能会发现这条查询语法在 H2 上没问题,却无法用于生产数据库 Postgres/Oracle。或者,你可能不得不维护多种不同的实现来缓解这个问题?这种测试实际上违背了测试的目的——就是为了让变更获得更快的反馈周期。
Now, welcome to the wonderful world of Testcontainers, where integration testing with real services is not only possible but also as easy as writing unit tests 🙂 Testcontainers is a testing library that provides easy and lightweight APIs for bootstrapping integration tests with real services wrapped in Docker containers. Using Testcontainers, you can write tests talking to the same type of services you use in production without mocks or in-memory services.
现在,欢迎来到 Testcontainers 的精彩世界。在这里,使用真实服务进行集成测试不仅成为可能,而且像编写单元测试一样简单 🙂 Testcontainers 是一个测试库,它提供了简单、轻量的 API,让你能够启动由 Docker 容器包裹的真实服务,从而进行集成测试。借助 Testcontainers,你可以编写与生产环境所用服务同一类型的测试,而无需 mock 或内存服务。
A typical Testcontainers-based integration test works as follows:
Before Tests:
Start your required services (databases, messaging systems etc.) docker containers using Testcontainers API.
Configure or update your application configuration to use these containerized services.
During Tests:
Your tests will run using these containerized services.
After Tests:
Testcontainers will take care of destroying those containers irrespective of whether tests executed successfully or there are any test failures.

一个典型的基于 Testcontainers 的集成测试流程如下:
测试前:
使用 Testcontainers API 启动所需服务(数据库、消息系统等)的 Docker 容器。
配置或更新应用配置,以使用这些容器化的服务。
测试期间:
你的测试将使用这些容器化的服务运行。
测试后:
无论测试成功还是失败,Testcontainers 都会负责销毁这些容器。

The only requirement to run Testcontainers-based tests is to have a Docker-API compatible container runtime. If you have Docker Desktop installed and running, you are good to go. For more information on Docker environments supported by Testcontainers refer to https://www.testcontainers.org/supported_docker_environment/.
运行基于 Testcontainers 的测试,唯一的要求是拥有一个兼容 Docker API 的容器运行时。如果你已经安装并运行了 Docker Desktop,那就万事俱备了。更多关于 Testcontainers 支持的 Docker 环境信息,可参阅 https://www.testcontainers.org/supported_docker_environment/。
What problems does Testcontainers solve? Testcontainers solves the integration testing problems mentioned above by enabling us to test our application using real services and thereby increasing the confidence level on our code changes.

Testcontainers 解决了什么问题?Testcontainers 通过让我们使用真实服务来测试应用,从而解决了上面提到的集成测试难题,并由此提升了我们对代码变更的信心。

By using Testcontainers:
You don’t need to have a pre-provisioned integration testing infrastructure. The Testcontainers API will provide the required services before running our tests. The code for defining the infrastructure resides directly next to the actual test code.
There will be no data conflict issues, even when multiple build pipelines run in parallel because each pipeline runs with an isolated set of services.
通过使用 Testcontainers:
你不再需要预先配置好的集成测试基础设施。Testcontainers API 会在运行测试前提供所需服务。定义基础设施的代码就放在实际测试代码旁边。
即使在多个构建管道并行运行时,也不会出现数据冲突问题,因为每个管道运行在一组隔离的服务中。
You can run your integration tests right from your IDE, just like you run unit tests. No need to push your changes and wait for CI to run your integration tests.
After test execution, Testcontainers take care of cleaning up the containers automatically.
You can use Testcontainers with many popular programming languages, including Java, .NET, Go, NodeJS, Rust, and Python, and more language support on its way.
你可以像运行单元测试一样,直接从 IDE 运行集成测试,无需推送代码变更后等待 CI 执行集成测试。
测试执行后,Testcontainers 会自动清理这些容器。
Testcontainers 可在许多主流编程语言中使用,包括 Java、.NET、Go、NodeJS、Rust 和 Python,未来还会支持更多语言。
Conclusion
We have explored the challenges with integration testing and understood why testing with mocks or in-memory services is not always a good idea. Then we talked about how Testcontainers solves the integration testing problem and enables us to test with real services.
结论
我们探讨了集成测试面临的挑战,理解了为什么使用 mock 或内存服务进行测试并不总是一个好主意。然后我们谈到了 Testcontainers 如何解决集成测试难题,并让我们能够用真实服务进行测试。