背景
最近在写Java单元测试的时候碰到了要和数据库打交道的场景,因为所写代码的实现依赖于数据库的相关功能(比如基于特定条件的查询),因此不能完全通过mock来覆盖测试。而在测试环境中不能直接链接到现在的开发环境,因而需要通过容器化的手段在CI环境中运行相应的单元测试。
在本例中,有两个运行场景:
- 本机Unit测试调试
- CI环境中的Unit测试
工具准备
使用到如下工具:
- Docker
- Testcontainers
- Maven
Docker 运行环境
基于testcontainers框架的描述,其对docker环境的要求如下:
| 主机操作系统/环境 | 最低推荐版本 | 已知问题/解决方法 |
|---|---|---|
| Linux - general | Docker v17.09 | |
| Linux - Travis CI | Docker v17.09 | See example .travis.yml for baseline Travis CI configuration |
| Linux - CircleCI (LXC driver) | Docker v17.09 | The exec feature is not compatible with CircleCI. See CircleCI configuration example |
| Linux - within a Docker container | Docker v17.09 | See Running inside Docker for Docker-in-Docker and Docker wormhole patterns |
| Mac OS X - Docker Toolbox | Docker Machine v0.8.0 | |
| Mac OS X - Docker for Mac | v17.09 | Support is best-efforts at present. getTestHostIpAddress() is not currently supported due to limitations in Docker for Mac. |
| Windows - Docker Toolbox | Support is limited at present and this is not currently tested on a regular basis. | |
| Windows - Docker for Windows | Support 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.09 | Support 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:2376DOCKER_TLS_VERIFY,默认值:DOCKER_TLS_VERIFY=1DOCKER_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
这样相当于对外开放的是 2375 端口,当然也可以根据自己情况修改成其他的。
2、 重新加载配置并重启
systemctl daemon-reload
systemctl restart docker
通过浏览器访问 2375 测试一下,格式为:http://ip:2375/version
更多配置参见: Docker开启远程安全访问
本地环境:
本例通过修改本地配置文件实现:
.testcontainers.properties置于用户的home目录,例如:
Linux:/home/myuser/.testcontainers.properties
Windows:C:/Users/myuser/.testcontainers.properties
macOS:/Users/myuser/.testcontainers.properties- 修改/添加属性
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);
}
}