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" );     } }