Spring Boot web項(xiàng)目的TDD流程
概述
測試驅(qū)動開發(fā)可以分為三個周期,周而復(fù)始,紅燈-綠燈-重構(gòu)。由以下幾個步驟構(gòu)成:
- 編寫測試
- 運(yùn)行所有測試
- 編寫代碼
- 運(yùn)行所有測試
- 重構(gòu)
- 運(yùn)行所有測試
一開始編寫測試,肯定通不過,紅燈狀態(tài),進(jìn)行代碼編寫,然后運(yùn)行測試,測試通不過,測試通過,即變成綠燈。
測試不通過,或者需要重構(gòu)代碼,再次運(yùn)行所有測試代碼...
接下來通過一個簡單的,一個RESTful請求的Spring boot web項(xiàng)目,演示和說明TDD的過程。
這個功能大致是這樣的,一個simple元素有id和desc兩個屬性
用戶發(fā)送GET請求http接口 http://localhost:8080/simples 返回所有的simple元素的json數(shù)組
1 技術(shù)工具
- JDK8+
- Spring Boot 2.1+
- maven or Gradle
- JPA
- JUnit 5+
- Mockito
- Hamcrest
一個常見的RESTful請求處理的MVC架構(gòu):
- 用戶訪問http url
- 通過Controller層接口
- Controller層調(diào)用Service的實(shí)現(xiàn)
- Service接口通過Repsoitory層訪問數(shù)據(jù)庫,并最終返回?cái)?shù)據(jù)給用戶
2 構(gòu)建Spring Boot工程
構(gòu)建一個Spring Boot Maven工程,并添加所需的依賴
參考依賴如下
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3 開始編寫測試和代碼
1 Controller
首先編寫測試Controller層的測試,test代碼區(qū)創(chuàng)建一個測試類,SimpleControllerTest
添加兩個注解 @ExtendWith和@WebMvcTest。
然后添加一個MockMvc對象,用來模擬mvc的請求。單元測試中,每個模塊應(yīng)當(dāng)獨(dú)立的測試,實(shí)際調(diào)用鏈中,Controller依賴Service層,因?yàn)楫?dāng)前測的是Controller層,對于Service層的代碼則進(jìn)行mock,這可以使用一個注解
@MockBean
整個代碼如下
@ExtendWith({SpringExtension.class})
@WebMvcTest
public class SimpleControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
private SimpleService simpleService;
}
SimpleService不存在,編譯不通過,紅燈,則創(chuàng)建它。
如是創(chuàng)建一個SimpleService作為Service層的Spring bean。
@Service
public class SimpleService {
}
然后編寫請求/simples http請求的測試代碼
@Test
void testFindAllSimples() throws Exception {
List<Simple> simpleList = new ArrayList<>();
simpleList.add(new Simple(1L,"one"));
simpleList.add(new Simple(2L,"two"));
when(simpleService.findAll()).thenReturn(simpleList);
mockMvc.perform(MockMvcRequestBuilders.get("/simples")
.contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$", hasSize(2))).andDo(print());
}
when then結(jié)構(gòu)來自Mockito框架,when表示了執(zhí)行的條件,then用于執(zhí)行驗(yàn)證,這里的操作對simpleService.findAll方法結(jié)果進(jìn)行了mock,這里 在這一層不需關(guān)心的simpleService的真實(shí)實(shí)現(xiàn)。后面perform方法 mock了 /simples的請求。
這里報(bào)錯,紅燈,接下來編寫Simple類的實(shí)現(xiàn)。
@Entity
public class Simple {
private Long id;
private String desc;
public Simple(String desc) {
this.desc = desc;
}
}
因?yàn)閟impleService.findAll方法未定義,所以還是報(bào)錯的,紅燈。接下來保持簡單,給SimpleService創(chuàng)建一個findAll方法。
public List<Simple> findAll() {
return new ArrayList<>();
}
編譯問題都解決了,下面開始運(yùn)行測試代碼。
報(bào)錯,
java.lang.AssertionError: No value at JSON path “$”
還是紅燈,這是因?yàn)槲覀僲ock的perform 沒有存在。接下來創(chuàng)建一個SimpleController類作為RestController,并編寫/simples請求的接口。
@RestController
public class SimpleController {
@Autowired
private SimpleService simpleService;
@GetMapping("/simples")
public ResponseEntity<List<Simple>> getAllSimples() {
return new ResponseEntity<>(simpleService.findAll(), HttpStatus.OK);
}
}
再次運(yùn)行測試用例,測試都通過了,綠燈。
2 Service
接下來讓我們關(guān)注Service層的代碼測試,test代碼區(qū)創(chuàng)建一個SimpleServiceTest類。該類對下一層Repository依賴,同樣的,創(chuàng)建一個Repository的mock對象。
@SpringBootTest
public class SimpleServiceTest {
@MockBean
private SimpleRepository simpleRepository;
}
編譯報(bào)錯,紅燈,需要創(chuàng)建一個SimpleRepository。
@Repository
public interface SimpleRepository extends JpaRepository<Simple,Long> {
}
以上,創(chuàng)建SimpleRepository作為實(shí)體Simple類對象的JPA存儲服務(wù)。
編寫測試代碼
@Test
void testFindAll() {
Simple simple = new Simple("one");
simpleRepository.save(simple);
SimpleService simpleService = new SimpleService(simpleRepository);
List<Simple> simples = simpleService.findAll();
Simple entity = simples.get(simples.size() - 1);
assertEquals(simple.getDesc(),entity.getDesc());
assertEquals(simple.getId(),entity.getId());
}
繼續(xù)解決編譯報(bào)錯的問題,SimpleService沒有構(gòu)造方法。添加Repository 并注入bean。
@Service
public class SimpleService {
private SimpleRepository simpleRepository;
public SimpleService(SimpleRepository simpleRepository) {
this.simpleRepository = simpleRepository;
}
public List<Simple> findAll() {
return new ArrayList<>();
}
}
這里插播一個題外話,為啥Spring推薦通過構(gòu)造方法的方式注入bean, 方便編寫可測試代碼是個重要原因。
運(yùn)行測試用例,會繼續(xù)報(bào)錯,這里是因?yàn)镴PA hibernate沒有和實(shí)體類對象交互,需要添加主鍵注解,默認(rèn)構(gòu)造函數(shù) getter/setter 重新編寫實(shí)體類的代碼。
@Entity
public class Simple {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String desc;
public Simple() {
}
public Simple(String desc) {
this.desc = desc;
}
// 省略 getter/setter ...
}
修改完畢之后 運(yùn)行測試用例 依然失敗,findAll方法測試未通過,修改SimpleService的findAll方法,調(diào)用 jpa repository的findAll方法
public List<Simple> findAll() {
return simpleRepository.findAll();
}
現(xiàn)在再次運(yùn)行測試用例,測試通過。
3 Repository
前面已經(jīng)通過了TDD去實(shí)現(xiàn)Controller層和Service層的代碼,理論上Repository實(shí)現(xiàn)了JPA的接口,我們沒有做任何代碼的編寫,應(yīng)該不需要進(jìn)行測試,但是我們不確定數(shù)據(jù)是否通過數(shù)據(jù)庫進(jìn)行了存儲和查詢。為了保證數(shù)據(jù)庫存儲,將真正的JPA respoitory實(shí)例注入的Service對象中。修改@MockBean 為@Autowired。
@SpringBootTest
public class SimpleServiceTest {
@Autowired
private SimpleRepository simpleRepository;
@Test
void testFindAll() {
Simple simple = new Simple("one");
simpleRepository.save(simple);
SimpleService simpleService = new SimpleService(simpleRepository);
List<Simple> simpleEntities = simpleService.findAll();
Simple entity = simpleEntities.get(simpleEntities.size() - 1);
assertEquals(simple.getDesc(),entity.getDesc());
assertEquals(simple.getId(),entity.getId());
}
}
創(chuàng)建H2 database配置。
classpath下 創(chuàng)建schema.sql和data.sql,創(chuàng)建表和插入一點(diǎn)數(shù)據(jù)。
#************H2 Begin**************** #創(chuàng)建表的MySql語句位置 spring.datasource.schema=classpath:schema.sql #插入數(shù)據(jù)的MySql語句的位置 spring.datasource.data=classpath:data.sql # 禁止自動根據(jù)entity創(chuàng)建表結(jié)構(gòu),表結(jié)構(gòu)由schema.sql控制 spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true
schema.sql
DROP TABLE IF EXISTS simple; CREATE TABLE `simple` ( id BIGINT(20) auto_increment, desc varchar(255) );
data.sql
INSERT INTO `simple`(`desc`) VALUES ('test1');
INSERT INTO `simple`(`desc`) VALUES ('test2');
繼續(xù)運(yùn)行測試用例,所有用例都測試通過,瀏覽器直接訪問localhost:8080/simples
返回data.sql插入的數(shù)據(jù)
[
{
"id": 1,
"desc": "test1"
},
{
"id": 2,
"desc": "test2"
}
]
4 總結(jié)
以上是一個完整的TDD開發(fā)流程的演示,每一個模塊的測試具備獨(dú)立性,當(dāng)前模塊中,可以mock其他模塊的數(shù)據(jù)。關(guān)于測試用例的結(jié)構(gòu),遵循的是AAA模式。
- Arrange: 單元測試的第一步,需要進(jìn)行必要的測試設(shè)置,譬如創(chuàng)建目標(biāo)類對象,必要時,創(chuàng)建mock對象和其他變量初始化等等
- Action: 調(diào)用要測試的目標(biāo)方法
- Assert: 單元測試的最后異步,檢查并驗(yàn)證結(jié)果與預(yù)期的結(jié)果是否一致。
以上就是Spring Boot web項(xiàng)目的TDD流程的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot web項(xiàng)目TDD的資料請關(guān)注腳本之家其它相關(guān)文章!
- 淺談測試驅(qū)動開發(fā)TDD之爭
- Android開發(fā)筆記之:對實(shí)踐TDD的一些建議說明
- QUnit jQuery的TDD框架
- SpringBoot內(nèi)置tomcat調(diào)優(yōu)測試優(yōu)化
- SpringBoot+redis配置及測試的方法
- SpringBoot對Controller進(jìn)行單元測試的實(shí)現(xiàn)代碼 附亂碼解決方案
- 詳解SpringBoot項(xiàng)目的創(chuàng)建與單元測試
- 解決SpringBoot 測試類無法自動注入@Autowired的問題
- Springboot Mybatis-Plus數(shù)據(jù)庫單元測試實(shí)戰(zhàn)(三種方式)
- Springboot集成JUnit5優(yōu)雅進(jìn)行單元測試的示例
- 使用@SpringBootTest注解進(jìn)行單元測試
- SpringBoot生產(chǎn)環(huán)境和測試環(huán)境配置分離的教程詳解
相關(guān)文章
SpringBoot如何優(yōu)雅的處理重復(fù)請求
對于一些用戶請求,在某些情況下是可能重復(fù)發(fā)送的,如果是查詢類操作并無大礙,但其中有些是涉及寫入操作的,一旦重復(fù)了,可能會導(dǎo)致很嚴(yán)重的后果,所以本文給大家介紹了SpringBoot優(yōu)雅的處理重復(fù)請求的方法,需要的朋友可以參考下2023-12-12
java解析xml的4種方式的優(yōu)缺點(diǎn)對比及實(shí)現(xiàn)詳解
這篇文章主要介紹了java解析xml的4種方式的優(yōu)缺點(diǎn)對比及實(shí)現(xiàn)詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-07-07
Java中的Unsafe在安全領(lǐng)域的使用總結(jié)和復(fù)現(xiàn)(實(shí)例詳解)
unsafe里面有很多好用的方法,比如allocateInstance可以直接創(chuàng)建實(shí)例對象,defineAnonymousClass可以創(chuàng)建一個VM匿名類(VM?Anonymous?Class),以及直接從內(nèi)存級別修改對象的值。這篇文章主要介紹了Java中的Unsafe在安全領(lǐng)域的一些應(yīng)用總結(jié)和復(fù)現(xiàn),需要的朋友可以參考下2022-03-03
Java CyclicBarrier源碼層分析與應(yīng)用
這篇文章主要介紹了Java CyclicBarrier的源碼層分析與應(yīng)用,CyclicBarrier也叫同步屏障,可以讓一組線程達(dá)到一個屏障時被阻塞,直到最后一個線程達(dá)到屏障,感興趣的的朋友可以參考下2023-12-12
JAVA統(tǒng)計(jì)字符串中某個字符出現(xiàn)次數(shù)的方法實(shí)現(xiàn)
本文主要介紹了JAVA統(tǒng)計(jì)字符串中某個字符出現(xiàn)次數(shù)的方法實(shí)現(xiàn),可以循環(huán)使用String的charAt(int index)函數(shù),具有一定的參考價值,感興趣的可以了解一下2023-11-11
Springboot之restTemplate的配置及使用方式
這篇文章主要介紹了Springboot之restTemplate的配置及使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10

