前言
1展蒂、何為Testcontainers?
Testcontainers是一個庫,它為引導(dǎo)本地開發(fā)和測試依賴關(guān)系提供了簡單而輕量級的API薄榛,并將真實的服務(wù)封裝在Docker容器中讳窟。使用Testcontainers吕朵,您可以編寫依賴于您在生產(chǎn)中使用的相同服務(wù)的測試南用,而不需要mock或內(nèi)存服務(wù)妆档。
用比較直白的話就是testcontainers 能夠讓你實現(xiàn)通過編程語言去啟動Docker容器宫屠,并在程序測試結(jié)束后哮塞,自動關(guān)閉容器
2隘世、Testcontainers有哪些優(yōu)勢掀鹅?
- 每個Test Group都能像寫單元測試那樣細(xì)粒度地寫集成測試丘逸,保證每個集成單元的高測試覆蓋率浦徊。
- Test Group間是做到依賴隔離的馏予,也就是說它們不共享任何一個Docker容器。
- 保證了生產(chǎn)環(huán)境和測試環(huán)境的一致性盔性,代碼部署到線上時不會遇到因為依賴服務(wù)接口不兼容而導(dǎo)致的bug 霞丧。
- Test Group可以并行化運(yùn)行,減少整體測試運(yùn)行時間冕香。相比較有些 in-memory的依賴服務(wù)實現(xiàn)沒有實現(xiàn)很好的資源隔離蛹尝,比如端口,一旦并行化運(yùn)行就會出現(xiàn)端口沖突 悉尾。
- 得益于Docker突那,所有測試都可以在本地環(huán)境和
CI/CD環(huán)境中運(yùn)行,測試代碼調(diào)試和編寫就如同寫單元測試构眯。 - 支持市面上主流的語言以及平臺愕难,比如java、go惫霸、python等
3猫缭、使用Testcontainers有哪些注意點(diǎn)
- Testcontainers基于Docker,所以使用Testcontainers前需要依賴Docker環(huán)境壹店。
- Testcontainers 提供的環(huán)境不能應(yīng)用于生產(chǎn)環(huán)境猜丹、只能用于測試環(huán)境等場景
4、Testcontainers連接docker的策略
Testcontainers在運(yùn)行時將會嘗試按如下順序使用以下策略連接到 Docker 守護(hù)程序:
環(huán)境變量:
– DOCKER_HOST
– DOCKER_TLS_VERIFY
– DOCKER_CERT_PATH
每個變量的作用:
- DOCKER_HOST to set the url to the docker server.
- DOCKER_CERT_PATH to load the tls certificates from.
- UseDOCKER_TLS_VERIFY to enable or disable TLS verification.
默認(rèn)值
– DOCKER_HOST=https://localhost:2376
– DOCKER_TLS_VERIFY=1
– DOCKER_CERT_PATH=~/.docker
我們可以通過環(huán)境變量修改以上值硅卢,示例
System.setProperty("DOCKER_HOST","tcp://192.168.0.1:2375")
注: 通過程序修改射窒,我們必須確保System.setProperty,在Testcontainers啟動容器之前就已經(jīng)設(shè)置将塑,否則無法生效
以上內(nèi)容可以在官網(wǎng)https://java.testcontainers.org/supported_docker_environment/查到更詳細(xì)的介紹
下面就以Testcontainers集成redis脉顿,并通過junit5進(jìn)行單元測試為例進(jìn)行演示
示例
1、項目中pom引入junit5 gav
<properties>
<junit-platform.version>1.9.2</junit-platform.version>
<junit-jupiter.version>5.9.2</junit-jupiter.version>
</properties>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>${junit-platform.version}</version>
<scope>test</scope>
</dependency>
注: 如果使用高本版的springboot抬旺,則可以直接引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>${junit-platform.version}</version>
<scope>test</scope>
</dependency>
即可
2弊予、項目中pom引入testcontainers gav
<properties>
<testcontainers.version>1.17.3</testcontainers.version>
</properties>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
當(dāng)然也需要引入redis客戶端 gav,因為這個大家應(yīng)該都知道开财,就不介紹了
3汉柒、在我們的單元測試中误褪,讓testcontainers運(yùn)行redis容器
示例代碼如下
@Container
private static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
.withExposedPorts(6379);
上面的代碼的意思是創(chuàng)建鏡像為redis:6.2.6容器,并將6379端口暴露出來
同時在測試類上碾褂,需要添加@Testcontainers(disabledWithoutDocker = true)
注解
@Testcontainers(disabledWithoutDocker = true)
public class RedisTest {
@Container
private static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
.withExposedPorts(6379);
}
4兽间、將我們業(yè)務(wù)程序能和容器集成
private Jedis jedis;
@BeforeEach
public void setUp() {
int port = redis.getMappedPort(6379);
jedis = new Jedis(redis.getHost(), port);
}
5、運(yùn)行單元測試
@Testcontainers(disabledWithoutDocker = true)
public class RedisTest {
@Container
private static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
.withExposedPorts(6379);
private Jedis jedis;
@BeforeEach
public void setUp() {
int port = redis.getMappedPort(6379);
jedis = new Jedis(redis.getHost(), port);
}
@AfterEach
public void tearDown() {
if (jedis != null) {
jedis.close();
}
}
@Test
public void testRedisConnectionAndSetAndGet() {
// 測試連接和簡單存取
String key = "testKey";
String value = "testValue";
jedis.set(key, value);
String result = jedis.get(key);
assert result.equals(value);
}
我們可以先觀察一下docker容器正塌,可以發(fā)現(xiàn)redis容器已經(jīng)成功運(yùn)行
再觀察一下單元測試結(jié)果嘀略,和我們預(yù)期一樣
單元測試結(jié)束后,我們再看下容器
發(fā)現(xiàn)容器已經(jīng)銷毀
上述的例子在官網(wǎng)也有詳細(xì)教程乓诽,可以查看如下鏈接
https://java.testcontainers.org/quickstart/junit_5_quickstart/
目前我們項目基本都是和springboot集成帜羊,接下來我們簡單演示一下testcontainers、springboot鸠天、redis集成
完整例子如下
@SpringBootTest(classes = TestcontainersApplication.class,webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Testcontainers(disabledWithoutDocker = true)
public class RedisContainerByDynamicPropertySourceTest {
@Autowired
private StringRedisTemplate redisTemplate;
@Container
private static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6.2.6"))
.withExposedPorts(6379);
// @BeforeEach
// public void setUp() {
//
// System.setProperty("spring.redis.host", redis.getHost());
// System.setProperty("spring.redis.port", redis.getMappedPort(6379).toString());
// }
/**
* Spring TEST 5.2.5才引入DynamicPropertySource
* @param registry
*/
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379)
.toString());
}
@Test
public void testRedisConnectionAndSetAndGet() {
// 測試連接和簡單存取
String key = "testKey";
String value = "testValue";
redisTemplate.opsForValue().set(key, value);
String result = redisTemplate.opsForValue().get(key);
assert Objects.equals(result, value);
}
}
核心的代碼是
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379)
.toString());
}
這個注解是spring5.2.5之后才有讼育,當(dāng)你事先不知道屬性的值時,通過@DynamicPropertySource和DynamicPropertyRegistry 搭配可以實現(xiàn)動態(tài)屬性綁定稠集。詳細(xì)介紹可以查看spring官網(wǎng)
https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/dynamic-property-sources.html
注: 如果springboot版本比較低奶段,則需要在項目pom引入如下gav,才能使用DynamicPropertySource
<spring.version>5.2.15.RELEASE</spring.version>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
查看單元測試結(jié)果
在使用Testcontainers踩到的坑
注: 因為我window沒開啟虛擬化剥纷,因此沒法安裝docker desktop痹籍。因此我的示例都是連接遠(yuǎn)程服務(wù)器進(jìn)行測試
因為要連接到遠(yuǎn)程的docker服務(wù)器,因此需要開啟2375端口晦鞋。開啟步驟如下
vim /usr/lib/systemd/system/docker.service
將默認(rèn)的
ExecStart=/usr/bin/dockerd -H unix://var/run/docker.sock \
修改為如下
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375
直接追加就行蹲缠,很多網(wǎng)上都是寫
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
這么寫會報錯。修改后鳖宾,執(zhí)行
systemctl daemon-reload
service docker restart
通過
ps -ef | grep docker
查看2375端口是否開啟
被挖過礦的朋友應(yīng)該會知道吼砂,很多宿主機(jī)就是因為公網(wǎng)暴露2375端口,結(jié)果被當(dāng)成礦機(jī)鼎文。因此可以通過ssh工具創(chuàng)建隧道,通過隧道訪問因俐。示例
不過我這邊也是因為通過隧道訪問拇惋,導(dǎo)致后面非常繁瑣
開始講解坑點(diǎn)
坑一:Testcontainers無法連接到遠(yuǎn)程docker
一開始我是通過
System.setProperty("DOCKER_HOST","tcp://192.168.0.1:2375")
進(jìn)行設(shè)置,因為我設(shè)置的點(diǎn)比Testcontainers創(chuàng)建容器的時間晚抹剩,因此導(dǎo)致Testcontainers連接的是本地docker撑帖,因為我本地沒安裝docker,導(dǎo)致無法連接上澳眷。
我們可以通過在idea上設(shè)置
不過有個博主更厲害胡嘿,他直接通過代碼修改。修改代碼內(nèi)容如下
注: 項目的pom需引入如下GAV
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.2.13</version>
</dependency>
/**
* testContainer的docker自定義連接策略
*/
@Slf4j
public class MyDockerClientProviderStrategy extends DockerClientProviderStrategy {
private final DockerClientConfig dockerClientConfig;
private static final String DOCKER_HOST = "tcp://127.0.0.1:2375";
/**
* 初始化的時候配置dockerClientConfig钳踊,我們通過docker-java來連接docker
*/
public MyDockerClientProviderStrategy() {
DefaultDockerClientConfig.Builder configBuilder = DefaultDockerClientConfig.createDefaultConfigBuilder();
configBuilder.withDockerHost(DOCKER_HOST);
//通過如下配置:關(guān)閉RYUK衷敌,解決Could not connect to Ryuk at localhost
System.setProperty("TESTCONTAINERS_RYUK_DISABLED","true");
// // 開啟dockerTLS校驗
// configBuilder.withDockerTlsVerify(true);
// // 密鑰所在文件夾勿侯,換到你的項目目錄中即可
// configBuilder.withDockerCertPath("C:\\Users\\Administrator\\Desktop\\docker");
dockerClientConfig = configBuilder.build();
}
/**
* 這里定義docker連接配置
*/
@Override
public TransportConfig getTransportConfig() {
return TransportConfig.builder()
.dockerHost(dockerClientConfig.getDockerHost())
.sslConfig(dockerClientConfig.getSSLConfig())
.build();
}
/**
* 對應(yīng)上面第二個filter,固定返回true即可缴罗。
*/
@Override
protected boolean isApplicable() {
return true;
}
@Override
public String getDescription() {
return "my-custom-strategy";
}
}
在src/main/resources創(chuàng)建META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy
文件內(nèi)容如下
com.github.lybgeek.testcontainers.MyDockerClientProviderStrategy
其實就spi機(jī)制助琐。那個博主的文章內(nèi)容如下,感興趣的朋友可以看看
https://blog.csdn.net/LHFFFFF/article/details/127117917
坑二:Could not connect to Ryuk at localhost
我也不懂這個是啥面氓,通過官方的issue
https://github.com/testcontainers/testcontainers-java/issues/3609#issuecomment-769615098
設(shè)置
TESTCONTAINERS_RYUK_DISABLED=true
禁用RYUK
關(guān)了貌似也沒啥影響
坑三:Timed out waiting for container port to open (localhost ports: [] should be listening)
一開始我是通過隧道訪問兵钮,后面發(fā)現(xiàn)每次啟動,testcontainer創(chuàng)建的容器端口會變舌界。示例
比如是端口32788掘譬,再啟動會變成32789。后面我就設(shè)置一段隨機(jī)端口的安全組呻拌,比如允許30000-40000端口段可以訪問葱轩。于是問題就暫時解決
總結(jié)
本文僅僅只是拋磚引玉,Testcontainers的官網(wǎng)有更多詳細(xì)的例子柏锄,大家感興趣可以去了解一下
https://testcontainers.com/guides/
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-testcontainers