使用容器测试ElasticSearch,基于Testcontainers

664 阅读4分钟

背景

最近在写Java单元测试的时候碰到了要和数据库打交道的场景,因为所写代码的实现依赖于数据库的相关功能(比如基于特定条件的查询),因此不能完全通过mock来覆盖测试。而在测试环境中不能直接链接到现在的开发环境,因而需要通过容器化的手段在CI环境中运行相应的单元测试。

在本例中,有两个运行场景:

  • 本机Unit测试调试
  • CI环境中的Unit测试

工具准备

使用到如下工具:

Docker 运行环境

基于testcontainers框架的描述,其对docker环境的要求如下:

主机操作系统/环境最低推荐版本已知问题/解决方法
Linux - generalDocker v17.09
Linux - Travis CIDocker v17.09See example .travis.yml for baseline Travis CI configuration
Linux - CircleCI (LXC driver)Docker v17.09The exec feature is not compatible with CircleCI. See CircleCI configuration example
Linux - within a Docker containerDocker v17.09See Running inside Docker for Docker-in-Docker and Docker wormhole patterns
Mac OS X - Docker ToolboxDocker Machine v0.8.0
Mac OS X - Docker for Macv17.09Support is best-efforts at present. getTestHostIpAddress() is not currently supported due to limitations in Docker for Mac.
Windows - Docker ToolboxSupport is limited at present and this is not currently tested on a regular basis.
Windows - Docker for WindowsSupport is best-efforts at present. Only Linux Containers (LCOW) are supported at the moment. See Windows Support
Windows - Windows Subsystem for Linux (WSL)Docker v17.09Support is best-efforts at present. Only Linux Containers (LCOW) are supported at the moment. See Windows Support.

Testcontainers会依次采用下面的策略去连接docker daemon:

  • 环境变量
    • DOCKER_HOST, 默认值: DOCKER_HOST=https://localhost:2376
    • DOCKER_TLS_VERIFY,默认值: DOCKER_TLS_VERIFY=1
    • DOCKER_CERT_PATH,默认值:DOCKER_CERT_PATH=~/.docker

Windows环境下的特殊配置

前置条件
  • Docker 已安装
  • Windows 10 下基于Hyper-v 的Docker 17.06 已确认可以正常使用
  • Testcontainers 支持通过命名管道的方式在Windows上支持Docker in Docker 。
  • Windows 10 2004下的WSL2后端可被支持(beta)
  • Windows server 2019 Docker不被支持
限制
  • 由于安全因素,MySQL 容器不支持定制配置文件(ini脚本)加载。
  • 基于Windows的容器不被支持

WSL (Windows LinuWx 子系统)

Testcontainers支持与基于WSL的Windows docker环境通信,但是需要下面这些额外的设置:

  • 将Docker for Windows 守护进程暴露与TCP端口2375,且需禁用TLS
  • 将WSL中的DOCKER_HOST环境变量设置为tcp://localhost:2375, 推荐通过~/.bashrc文件设置

连接外部Dockers环境的特殊配置

如需在测试的时候使用外部Docker环境,则需要下面的特殊配置:

远程环境:
1、编辑Docker服务配置:
vi /usr/lib/systemd/system/docker.service #不同操作系统不一样,需要根据实际情况修改

找到 Service 节点,修改 ExecStart 属性,增加 -H tcp://0.0.0.0:2375

Pasted image 20220510110450.png 这样相当于对外开放的是 2375 端口,当然也可以根据自己情况修改成其他的。

2、 重新加载配置并重启
systemctl daemon-reload
systemctl restart docker

通过浏览器访问 2375 测试一下,格式为:http://ip:2375/version

更多配置参见: Docker开启远程安全访问

本地环境:

本例通过修改本地配置文件实现:

  1. .testcontainers.properties 置于用户的home目录,例如:
    Linux: /home/myuser/.testcontainers.properties
    Windows: C:/Users/myuser/.testcontainers.properties
    macOS: /Users/myuser/.testcontainers.properties
  2. 修改/添加属性
docker.host=tcp://10.0.0.4:2375 #<remote_ip>t:<remote_port>,与DOCKER_HOST一致
docker.tls.verify=1 # 与DOCKER_TLS_VERIFY 一致
docker.cert.path=/some/path # 与 DOCKER_CERT_PATH 一致

基于Spring Boot 的示例

添加Maven依赖

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.16.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.16.3</version>
    <scope>test</scope>
</dependency>

定制ApplicationContextInitializer

因为需要动态注入有关container的信息,因此我们需要定制自己的ApplicationContextInitializer:

public class ElasticInitializer  
      implements ApplicationContextInitializer<ConfigurableApplicationContext> {  
   @ClassRule  
   public static ElasticsearchContainer container = new ElasticsearchContainer();  
  
   /**  
    * @param applicationContext  
    */  
   @Override  
   public void initialize(ConfigurableApplicationContext applicationContext) {  
      container.start();  
      TestPropertyValues.of("elasticsearch.host=" + container.getHost(),  
                  "elasticsearch.port=" + container.getMappedPort(9200))  
            .applyTo(applicationContext);  
   }  
}

编写测试用例


@Testcontainers  //通知Junit自动开启和关闭容器
@SpringBootTest  //Springboot 测试入口
@ActiveProfiles(profiles = {"test"}) // 指定测试profile  
@ContextConfiguration(initializers = {ElasticInitializer.class}) // 指定额外的initializer,以注入容器相关值  
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)  //采取指定顺序运行测试用例
class PMBServiceImplTest {  
  
   @Autowired  
   private PMBServiceImpl pmbServiceImplUnderTest;  
  
   private PmbRawData pmbRawData;  
   private PmPkgLoaderImpl pmPkgLoader;  
  
  
   @BeforeEach  
   void setUp() throws Exception {  
      pmPkgLoader = new PmPkgLoaderImpl();  
      pmbRawData = pmPkgLoader.loadPackage("src/test/resources/adaptations/adaptation_com.eric.cnsdla.pmb-22.0.0-20220427T044952.zip", PmbRawData.class);  
  
   }  
  
   @Test  
   @Order(1)  
   void testDeploy() {  
  
      // Run the test  
      final PmbRawData result = pmbServiceImplUnderTest.deploy(pmbRawData);  
  
      // Verify the results  
      assertEquals(result, pmbRawData);  
   }  
  
   @Test  
   @Order(100)  
   void testUndeploy1() {  
      // Setup  
      // Run the test      //adaptation_com.nokia.cnsdla.pmb-22.0.0-20220427T044952.zip      pmbServiceImplUnderTest.undeploy("com.eric.cnsdla", "22.0.0", "20220427T044952");  
  
      // Verify the results  
      PmbRawData result = pmbServiceImplUnderTest.getPmb("com.eric.cnsdla", "22.0.0", "20220427T044952");  
      Assertions.assertNull(result);  
   }  
  
   @Test  
   @Order(101)  
   void testUndeploy2() {  
      // Setup  
      // Run the test  
      assertThrows(UnsupportedOperationException.class, new Executable() {  
         @Override  
         public void execute() throws Throwable {  
            pmbServiceImplUnderTest.undeploy("adaptationId", "version");  
         }  
      });  
      // Verify the results  
   }  
  
   @Test  
  
   @Order(2)  
   void testGetPmb() {  
      // Setup  
      // Configure  
	 PmbRepository.getDistinctFirstByAdaptationIdAndReleaseAndVersion(...).  
  
      // Run the test      
      final PmbRawData result = pmbServiceImplUnderTest.getPmb("com.eric.cnsdla", "22.0.0", "20220427T044952");  
  
      // Verify the results  
      assertEquals(result.toString(), pmbRawData.toString());  
   }  
  
   @Test  
  
   @Order(3)  
   void testGetActivePmbList() throws IOException {  
      // Setup  
  
      // Run the test
      final List<PmbRawData> result = pmbServiceImplUnderTest.getActivePmbList("com.eric.cnsdla");  
  
      // Verify the results  
   }  
  
   @Test  
  
   @Order(4)  
   void testGetActivePmbList_ElasticsearchOperationsReturnsNoItems() throws IOException {  
      // Setup  
  
  
      // Run the test
      final List<PmbRawData> result = pmbServiceImplUnderTest.getActivePmbList("com.eric.dummy");  
  
      // Verify the results  
      assertEquals(null, result);  
   }  
}