testcontainer

在我們的項目中,單元測試是保證我們代碼質量非常重要的一環,但是我們的業務代碼不可避免的需要依賴外部的系統或服務如DB,redis,其他外部服務等。如何保證我們的測試代碼不受外部依賴的影響,能夠穩定的運行成為了一件比較讓人頭疼的事情。

mock

通過mockito等框架,可以模擬外部依賴各類組件的返回值,通過隔離的方式穩定我們的單元測試。但是這種方式也會帶來以下問題:

  1. 測試代碼冗長難懂,因為要在測試中模擬各類返回值,一行業務代碼往往需要很多測試代碼來支撐
  2. 無法真實的執行SQL腳本
  3. 僅僅適用于單元測試,對于端到端的測試無能為力

testcontainer

testcontainer,人如其名,可以在啟動測試時,創建我們所依賴的外部容器,在測試結束時自動銷毀,通過容器的技術來達成測試環境的隔離。

目前testcontainer僅支持docker,使用testcontainer需要提前安裝docker環境

簡單的例子

假設我們有一個springboot的web項目,在這個項目中使用postgresql作為數據庫
首先我們將testcontainer添加到pom.xml中

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>postgresql</artifactId><scope>test</scope></dependency><dependency><groupId>io.rest-assured</groupId><artifactId>rest-assured</artifactId><scope>test</scope></dependency></dependencies>

然后是我們的entity對象和Repository

@Entity
@Table(name = "customers")
public class Customer {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false)private String name;@Column(nullable = false, unique = true)private String email;public Customer() {}public Customer(Long id, String name, String email) {this.id = id;this.name = name;this.email = email;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}
}
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

最后是我們的controller

@RestController
public class CustomerController {private final CustomerRepository repo;CustomerController(CustomerRepository repo) {this.repo = repo;}@GetMapping("/api/customers")List<Customer> getAll() {return repo.findAll();}
}

由于是demo,我們通過src/resources/schema.sql來進行表的初始化,在正常項目中應該通過flyway等進行數據庫的管理

create table if not exists customers (id bigserial not null,name varchar not null,email varchar not null,primary key (id),UNIQUE (email)
);

并在application.properties中添加如下配置

spring.sql.init.mode=always

最后是重頭戲,如果通過testcontainer來完成我們的端到端測試

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;import com.example.testcontainer.domain.Customer;
import com.example.testcontainer.repo.CustomerRepository;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerTest {@LocalServerPortprivate Integer port;static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14");@BeforeAllstatic void beforeAll() {postgres.start();}@AfterAllstatic void afterAll() {postgres.stop();}@DynamicPropertySourcestatic void configureProperties(DynamicPropertyRegistry registry) {registry.add("spring.datasource.url", postgres::getJdbcUrl);registry.add("spring.datasource.username", postgres::getUsername);registry.add("spring.datasource.password", postgres::getPassword);}@AutowiredCustomerRepository customerRepository;@BeforeEachvoid setUp() {RestAssured.baseURI = "http://localhost:" + port;customerRepository.deleteAll();}@Testvoid shouldGetAllCustomers() {List<Customer> customers = List.of(new Customer(null, "John", "john@mail.com"),new Customer(null, "Dennis", "dennis@mail.com"));customerRepository.saveAll(customers);given().contentType(ContentType.JSON).when().get("/api/customers").then().statusCode(200).body(".", hasSize(2));}
}

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
“postgres:14”
);`

這里創建了一個postgres的容器,然后通過 @DynamicPropertySourcespring.datasource的各項參數賦值,剩下的就由Spring的auto-configure完成各類bean的自動裝配,JPA的裝配和注入。

除了通過 @DynamicPropertySource外,spring-boot-testcontainers提供了一些方法可以更加簡化這個流程
首先添加依賴

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-testcontainers</artifactId><scope>test</scope></dependency>
@Testcontainers
@SpringBootTest
public class MyIntegrationServiceConnectionTests {@Container@ServiceConnectionstatic PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14");@AutowiredCustomerRepository customerRepository;@Testvoid shouldGetAllCustomers() {List<Customer> customers = List.of(new Customer(null, "John", "john@mail.com"),new Customer(null, "Dennis", "dennis@mail.com"));customerRepository.saveAll(customers);assertTrue(customerRepository.findByName("John").isPresent());assertEquals(2,customerRepository.findAll().size());}
}

@Testcontainers注解會幫助我們在測試開始前啟動容器,在測試結束后停止容器,因此我們可以省略@BeforeAll和@AfterAll

@ServiceConnection取代了 @DynamicPropertySource中代碼的功能,幫助我們完成bean的創建

除此之外,我們也可以通過@bean的方式來創建容器

@Testcontainers
@SpringBootTest
public class MyIntegrationBeanConfiguratonTests {@AutowiredCustomerRepository customerRepository;@Testvoid shouldGetAllCustomers() {List<Customer> customers = List.of(new Customer(null, "John", "john@mail.com"),new Customer(null, "Dennis", "dennis@mail.com"));customerRepository.saveAll(customers);assertTrue(customerRepository.findByName("John").isPresent());assertEquals(2,customerRepository.findAll().size());}@TestConfiguration(proxyBeanMethods = false)public  static class MyPostgreConfiguration {@Bean@ServiceConnectionpublic PostgreSQLContainer<?> postgreSQLContainer() {return new PostgreSQLContainer<>("postgres:14");}}
}

連接web服務

PostgreSQLContainer是一個專門用于連接PostgreSQL的類,除此之外testcontainer還提供了redis、mysql、es、kafka等常用中間件的容器類。如果我們想要去連接一個內部的web服務,那該怎么做?

首先,我們要確保該服務已經容器化,可以直接通過docker來啟動,其次,testcontainer提供了一個公共的GenericContainer來處理這類場景,

假設我們有一個服務鏡像demo/demo:latest,該服務暴露了一個8080端口,我們通過feignclient來訪問

@FeignClient(name = "customFeign",url = "${remote.custom.url}")
public interface ExternalCustomClient {@GetMapping("custom/{id}")public CustomInfo getCustom(@PathVariable("id") String id);
}
@Testcontainers
@SpringBootTest
public class ExternalCustomClientTest {@Containerstatic GenericContainer<?> container = new GenericContainer<>("demo/demo:latest").withExposedPorts(8080);@DynamicPropertySourcestatic void configureProperties(DynamicPropertyRegistry registry) {Integer firstMappedPort = container.getMappedPort(8080);String ipAddress = container.getHost();System.out.println(ipAddress);System.out.println(firstMappedPort);registry.add("remote.custom.url",() -> "http://"+ipAddress+":"+firstMappedPort);}@AutowiredExternalCustomClient customClient;@Testvoid shouldGetAllCustomers() {String id ="111";CustomInfo customInfo=customClient.getCustom(id);Assertions.assertEquals(id, customInfo.getCustomNo());}}

由于使用了GenericContainer,Springboot不知道該如何去連接容器,因此不能使用@ServiceConnection注解,還是回到@DynamicPropertySource的方式。

@Container
static GenericContainer<?> container = new GenericContainer<>(
“zhangxiaotian/caizi:latest”
).withExposedPorts(8080);

在容器內部監聽了一個8080端口,這個需要和外部服務的端口一致,對容器外部暴露的端口,我們可以通過

Integer firstMappedPort = container.getMappedPort(8080);
String ipAddress = container.getHost();

來獲取完整的ip和端口

完整的代碼示例可以通過以下倉庫獲取
https://gitee.com/xiiiao/testcontainer-demo

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/21011.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/21011.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/21011.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

c++------類和對象(下)包含了this指針、構造函數、析構函數、拷貝構造等

文章目錄 前言一、this指針1.1、this指針的引出1.2、 this指針的特性 二、類的默認的六個構造函數2.1、構造函數簡述2.2構造函數 三、析構函數3.1、析構函數引出3.2、特點&#xff1a; 四、拷貝構造4.1、引入4.2、特征&#xff1a;4.3、默認拷貝構造函數 總結 前言 在本節中&a…

中國的歷史看中國的經濟發展

從中國的歷史看中國的經濟發展&#xff0c;可以發現其經歷了幾個顯著的階段&#xff0c;每個階段都有其獨特的特點和成就&#xff1a; 古代經濟&#xff1a;中國古代經濟以農業為主&#xff0c;實行井田制&#xff0c;重視水利工程的建設&#xff0c;如都江堰、靈渠等。 商業發…

Compose Multiplatform 1.6.10 發布,解釋一些小問題, Jake 大佬的 Hack

雖然一直比較關注跨平臺開發&#xff0c;但其實我很少寫 Compose Multiplatform 的內容&#xff0c;因為關于 Compose Multiplatform 的使用&#xff0c;其實我并沒在實際生產環境上發布過&#xff0c;但是這個版本確實值得一提&#xff0c;因為該版本包含&#xff1a; iOS Bet…

數據庫(15)——DQL分頁查詢

DQL分頁查詢語法 SELECT 字段列表 FROM 表名 LIMIT 起始索引&#xff0c;查詢記錄數; 注&#xff1a;起始索引從0開始&#xff0c;起始索引&#xff08;查詢頁碼-1&#xff09;*每頁顯示記錄數。 如果查詢的是第一頁&#xff0c;可以省略起始索引。 示例&#xff1a;查詢第一頁…

【考研數學】概率論如何復習?跟誰好?

概率論一定要跟對老師&#xff0c;如果跟對老師&#xff0c;考研基本上能拿滿分 概率論在考研試卷中占比并不大&#xff0c;其中&#xff1a; 高等數學&#xff0c;90分&#xff0c;約占比60%; 線性代數&#xff0c;30分&#xff0c;約占比20%; 概率論與數理統計&#xff0…

hive中的join操作及其數據傾斜

hive中的join操作及其數據傾斜 join操作是一個大數據領域一個常見的話題。歸根結底是由于在數據量超大的情況下&#xff0c;join操作會使內存占用飆升。運算的復雜度也隨之上升。在進行join操作時&#xff0c;也會更容易發生數據傾斜。這些都是需要考慮的問題。 過去了解到很…

每日5題Day15 - LeetCode 71 - 75

每一步向前都是向自己的夢想更近一步&#xff0c;堅持不懈&#xff0c;勇往直前&#xff01; 第一題&#xff1a;71. 簡化路徑 - 力扣&#xff08;LeetCode&#xff09; class Solution {public String simplifyPath(String path) {Deque<String> stack new LinkedList…

mysql的增刪查改(進階)

目錄 一. 更復雜的新增 二. 查詢 2.1 聚合查詢 COUNT SUM AVG MAX MIN 2.1.2 分組查詢 group by 子句 2.1.3 HAVING 2.2 聯合查詢/多表查詢 2.2.1 內連接 2.2.2 外連接 2.2.3 全外連接 2.2.4 自連接 2.2.5 子查詢 2.2.6 合并查詢 一. 更復雜的新增 將從表名查詢到…

自動化辦公01 smtplib 郵件?動發送

目錄 一、準備需要發送郵件的郵箱賬號 二、發送郵箱的基本步驟 1. 登錄郵箱 2. 準備數據 3. 發送郵件 三、特殊內容的發送 1. 發送附件 2. 發送圖片 3. 發送超文本內容 4.郵件模板內容 SMTP&#xff08;Simple Mail Transfer Protocol&#xff09;即簡單郵件傳輸協議…

霍夫曼樹教程(個人總結版)

背景 霍夫曼樹&#xff08;Huffman Tree&#xff09;是一種在1952年由戴維霍夫曼&#xff08;David A. Huffman&#xff09;提出的數據壓縮算法。其主要目的是為了一種高效的數據編碼方法&#xff0c;以便在最小化總編碼長度的情況下對數據進行編碼。霍夫曼樹通過利用出現頻率…

【Qt秘籍】[009]-自定義槽函數/信號

自定義槽函數 在Qt中自定義槽函數是一個直接的過程&#xff0c;槽函數本質上是類的一個成員函數&#xff0c;它可以響應信號。所謂的自定義槽函數&#xff0c;實際上操作過程和定義普通的成員函數相似。以下是如何在Qt中定義一個自定義槽函數的步驟&#xff1a; 步驟 1: 定義槽…

<jsp:setProperty>設置有參構造函數創建的自定義對象的屬性

假設某一個類&#xff08;如TextConverter類&#xff09;有一個無參構造函數和一個有參構造函數&#xff0c;我們可以在Servlet里面先用有參構造函數自己new一個對象出來&#xff0c;存到request.setAttribute里面去。 Servlet轉發到jsp頁面后&#xff0c;再在jsp頁面上用<j…

django基于大數據+Spring的新冠肺炎疫情實時監控系統設計和實現

設計一個基于Django(后端)和Spring(可能的中間件或服務集成)的新冠肺炎疫情實時監控系統涉及多個方面,包括數據收集、數據處理、數據存儲、前端展示以及可能的中間件服務(如Spring Boot服務)。以下是一個大致的設計和實現步驟: 1. 系統架構 前端:使用Web框架(如Reac…

三種字符串的管理方式

NSString的三種實現方式 OC這個語言在不停的升級自己的內存管理&#xff0c;盡量的讓自己的 OC的字符串 問題引入 在學習字符串的過程中間會遇到一個因為OC語言更新造成的問題 例如&#xff1a; int main(int argc, const char * argv[]) {autoreleasepool {NSString* str1 …

C++核心編程類的總結封裝案例

C類的總結封裝案例 文章目錄 C類的總結封裝案例1.立方體類的封裝2.點與圓的關系的封裝3.總結 1.立方體類的封裝 在C中&#xff0c;我們可以定義一個立方體&#xff08;Cube&#xff09;類來封裝立方體的屬性和方法。立方體的屬性可能包括邊長&#xff08;side length&#xff…

【redis】set和zset常用命令

set 無序集合類型 sadd 和 smembers SADD&#xff1a;將一個或者多個元素添加到set中。注意,重復的元素無法添加到set中。 語法&#xff1a;SADD key member [member] 把集合中的元素,叫做member,就像hash類型中,叫做field類似. 返回值表示本次操作,添加成功了幾個元素. 時間復…

網絡原理——http/https ---http(1)

T04BF &#x1f44b;專欄: 算法|JAVA|MySQL|C語言 &#x1faf5; 今天你敲代碼了嗎 網絡原理 HTTP/HTTPS HTTP,全稱為"超文本傳輸協議" HTTP 誕?與1991年. ?前已經發展為最主流使?的?種應?層協議. 實際上,HTTP最新已經發展到 3.0 但是當前行業中主要使用的HT…

概念解析 | 為什么SAR中的天線間隔需要是四分之一波長?

注1:本文系“概念解析”系列之一,致力于簡潔清晰地解釋、辨析復雜而專業的概念。本次辨析的概念是:為什么SAR中的天線間隔需要是四分之一波長 概念解析 | 為什么SAR中的天線間隔需要是四分之一波長? 在這篇文章中,我們將深入探討**合成孔徑雷達(SAR)**系統中,為什么天…

明日周刊-第12期

以前小時候最期待六一兒童節了&#xff0c;父母總會給你滿足一個愿望&#xff0c;也許是一件禮物也許是一次陪伴。然而這個世界上其實還有很多兒童過不上兒童節&#xff0c;比如某些地區的小孩子&#xff0c;他們更擔心的是能不能見到明天的太陽。 文章目錄 一周熱點航天探索火…

LeetCode-77. 組合【回溯】

LeetCode-77. 組合【回溯】 題目描述&#xff1a;解題思路一&#xff1a;回溯背誦版解題思路三&#xff1a;0 題目描述&#xff1a; 給定兩個整數 n 和 k&#xff0c;返回范圍 [1, n] 中所有可能的 k 個數的組合。 你可以按 任何順序 返回答案。 示例 1&#xff1a; 輸入&a…