Testcontainers for Java简介 Java的Testcontainers 是一个支持JUnit测试的Java库,提供通用数据库,Selenium Web浏览器或任何其他可以在Docker容器中运行的轻量级一次性实例。
Testcontainers 能够让你实现通过编程语言去启动Docker容器,并在程序测试结束后,自动关闭容器。这基本上能解决我们大部分的需求。
使用 TestContainers 这种解决方案还有以下几个优点:
每个Test Group都能像写单元测试那样细粒度地写集成测试,保证每个集成单元的高测试覆盖率。
Test Group间是做到依赖隔离的,也就是说它们不共享任何一个Docker容器;假如两个Test Group都要用到Mongo 4.0,会创建两个容器供它们单独使用 。
保证了生产环境和测试环境的一致性,代码部署到线上时不会遇到因为依赖服务接口不兼容而导致的bug 。
Test Group可以并行化运行,减少整体测试运行时间。相比较有些 in-memory 的依赖服务实现没有实现很好的资源隔离,比如端口,一旦并行化运行就会出现端口冲突 。
得益于Docker,所有测试都可以在本地环境和 CI/CD环境中运行,测试代码调试和编写就如同写单元测试。
测试容器使以下类型的测试更容易:
数据访问层集成测试 :使用 MySQL、PostgreSQL 或 Oracle 数据库的容器化实例来测试数据访问层代码的完全兼容性,但不需要在开发人员的计算机上进行复杂的设置,并且知道您的测试将始终从已知的数据库状态开始。也可以使用任何其他可以容器化的数据库类型。
应用程序集成测试 :用于在具有依赖项(如数据库、消息队列或 Web 服务器)的短期测试模式下运行应用程序。
UI/验收测试 :使用与Selenium兼容的容器化Web浏览器 进行自动化UI测试。每个测试都可以获得浏览器的新实例,无需担心浏览器状态、插件变体或自动浏览器升级。您可以获得每个测试会话的视频记录,或者只是测试失败的每个会话。
先决条件
Maven 依赖项 对于核心库,最新的 Maven/Gradle 依赖项如下:
1 2 3 4 5 6 <dependency > <groupId > org.testcontainers</groupId > <artifactId > testcontainers</artifactId > <version > 1.18.3</version > <scope > test</scope > </dependency >
管理多个测试容器依赖项的版本 若要避免指定每个依赖项的版本,可以使用BOM
或Bill Of Materials
使用 Maven,您可以将以下dependencyManagement
添加到您的pom.xml
1 2 3 4 5 6 7 8 9 10 11 <dependencyManagement > <dependencies > <dependency > <groupId > org.testcontainers</groupId > <artifactId > testcontainers-bom</artifactId > <version > 1.18.3</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement >
然后在不指定版本的情况下使用依赖项:
1 2 3 4 5 <dependency > <groupId > org.testcontainers</groupId > <artifactId > mysql</artifactId > <scope > test</scope > </dependency >
JUnit 4 快速入门 假设我们有一个简单的程序依赖于 Redis,我们想为它添加一些测试。 在我们的虚构程序中,有一个类在 Redis 中存储数据。
RedisBackedCache
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public interface Cache { void put (String key, Object value) ; <T> Optional<T> get (String key, Class<T> expectedClass) ; } public class RedisBackedCache implements Cache { private final Jedis jedis; private final String cacheName; private final Gson gson; public RedisBackedCache (Jedis jedis, String cacheName) { this .jedis = jedis; this .cacheName = cacheName; this .gson = new Gson (); } @Override public void put (String key, Object value) { String jsonValue = gson.toJson(value); this .jedis.hset(this .cacheName, key, jsonValue); } @Override public <T> Optional<T> get (String key, Class<T> expectedClass) { String foundJson = this .jedis.hget(this .cacheName, key); if (foundJson == null ) { return Optional.empty(); } return Optional.of(gson.fromJson(foundJson, expectedClass)); } }
Maven依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <dependencies > <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > <version > 4.3.1</version > </dependency > <dependency > <groupId > org.testcontainers</groupId > <artifactId > testcontainers</artifactId > <version > 1.18.3</version > <scope > test</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency > <dependency > <groupId > org.assertj</groupId > <artifactId > assertj-core</artifactId > <version > 3.24.2</version > <scope > test</scope > </dependency > </dependencies >
您可以看到一个可能为其编写的示例测试(不使用 Testcontainers):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class RedisBackedCacheIntTestStep0 { private RedisBackedCache underTest; @Before public void setUp () throws Exception { Jedis jedis = new Jedis ("localhost" , 6379 ); underTest = new RedisBackedCache (jedis, "test" ); } @Test public void testSimplePutAndGet () { underTest.put("test" , "example" ); String retrieved = underTest.get("test" ); assertThat(retrieved).isEqualTo("example" ); } }
请注意,现有测试存在问题 - 它依赖于 Redis 的本地安装,这是测试可靠性的危险信号。 如果我们确定每个开发人员和 CI 计算机都安装了 Redis,这可能会起作用,否则会失败。 如果我们尝试并行运行测试,我们也可能会遇到问题,例如测试之间的状态出错或端口冲突。
使用 Testcontainers 改进测试:
让测试容器在我们的测试期间运行 Redis 容器 JUnit 4 规则
1 2 3 4 @Rule public GenericContainer redis = new GenericContainer (DockerImageName.parse("redis:7.0.5" )) .withExposedPorts(6379 );
@Rule
注释告诉 JUnit 通知此字段有关测试生命周期中的各种事件。 在这种情况下,我们的规则对象是一个 测试容器GenericContainer
,配置为使用 Docker Hub 中的特定 Redis 映像,并配置为公开端口。
如果我们按原样运行测试,那么无论实际测试结果如何,我们都会看到日志,显示 Testcontainers:
在我们的测试方法运行之前被激活
发现并快速测试了我们的本地 Docker 设置
如有必要,拉取图像
启动一个新容器并等待它准备就绪
测试后关闭并删除容器
确保我们的代码可以与容器通信 在 Testcontainers 之前,我们可能已经将一个地址硬编码到我们的测试中。localhost:6379
Testcontainers 对其启动的每个容器使用随机端口,但可以轻松地在运行时获取实际端口 。 我们可以在测试方法中执行此操作,以设置被测试组件:setUp
获取映射端口
1 2 3 4 5 6 String address = redis.getHost();Integer port = redis.getFirstMappedPort();underTest = new RedisBackedCache (address, port);
运行测试! RedisBackedCacheIntTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class RedisBackedCacheTest { @Rule public GenericContainer<?> redis = new GenericContainer <>(DockerImageName.parse("redis:7.0.5" )) .withExposedPorts(6379 ); private Cache cache; @Before public void setUp () throws Exception { Jedis jedis = new Jedis (redis.getHost(), redis.getMappedPort(6379 )); cache = new RedisBackedCache (jedis, "test" ); } @Test public void testFindingAnInsertedValue () { cache.put("foo" , "FOO" ); Optional<String> foundObject = cache.get("foo" , String.class); assertThat(foundObject.isPresent()).as("When an object in the cache is retrieved, it can be found" ).isTrue(); assertThat(foundObject.get()) .as("When we put a String in to the cache and retrieve it, the value is the same" ) .isEqualTo("FOO" ); } }