Spring Boot web項(xiàng)目的TDD流程
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)可以分為三個(gè)周期,周而復(fù)始,紅燈-綠燈-重構(gòu)。由以下幾個(gè)步驟構(gòu)成:
編寫(xiě)測(cè)試 運(yùn)行所有測(cè)試 編寫(xiě)代碼 運(yùn)行所有測(cè)試 重構(gòu) 運(yùn)行所有測(cè)試一開(kāi)始編寫(xiě)測(cè)試,肯定通不過(guò),紅燈狀態(tài),進(jìn)行代碼編寫(xiě),然后運(yùn)行測(cè)試,測(cè)試通不過(guò),測(cè)試通過(guò),即變成綠燈。
測(cè)試不通過(guò),或者需要重構(gòu)代碼,再次運(yùn)行所有測(cè)試代碼...
接下來(lái)通過(guò)一個(gè)簡(jiǎn)單的,一個(gè)RESTful請(qǐng)求的Spring boot web項(xiàng)目,演示和說(shuō)明TDD的過(guò)程。
這個(gè)功能大致是這樣的,一個(gè)simple元素有id和desc兩個(gè)屬性
用戶(hù)發(fā)送GET請(qǐng)求http接口 http://localhost:8080/simples 返回所有的simple元素的json數(shù)組
1 技術(shù)工具 JDK8+ Spring Boot 2.1+ maven or Gradle JPA JUnit 5+ Mockito Hamcrest一個(gè)常見(jiàn)的RESTful請(qǐng)求處理的MVC架構(gòu):
用戶(hù)訪問(wèn)http url 通過(guò)Controller層接口 Controller層調(diào)用Service的實(shí)現(xiàn) Service接口通過(guò)Repsoitory層訪問(wèn)數(shù)據(jù)庫(kù),并最終返回?cái)?shù)據(jù)給用戶(hù) 2 構(gòu)建Spring Boot工程構(gòu)建一個(gè)Spring Boot Maven工程,并添加所需的依賴(lài)
參考依賴(lài)如下
<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 開(kāi)始編寫(xiě)測(cè)試和代碼1 Controller
首先編寫(xiě)測(cè)試Controller層的測(cè)試,test代碼區(qū)創(chuàng)建一個(gè)測(cè)試類(lèi),SimpleControllerTest
添加兩個(gè)注解 @ExtendWith和@WebMvcTest。
然后添加一個(gè)MockMvc對(duì)象,用來(lái)模擬mvc的請(qǐng)求。單元測(cè)試中,每個(gè)模塊應(yīng)當(dāng)獨(dú)立的測(cè)試,實(shí)際調(diào)用鏈中,Controller依賴(lài)Service層,因?yàn)楫?dāng)前測(cè)的是Controller層,對(duì)于Service層的代碼則進(jìn)行mock,這可以使用一個(gè)注解
@MockBean
整個(gè)代碼如下
@ExtendWith({SpringExtension.class})@WebMvcTestpublic class SimpleControllerTest { @Autowired MockMvc mockMvc; @MockBean private SimpleService simpleService;}
SimpleService不存在,編譯不通過(guò),紅燈,則創(chuàng)建它。
如是創(chuàng)建一個(gè)SimpleService作為Service層的Spring bean。
@Servicepublic class SimpleService {}
然后編寫(xiě)請(qǐng)求/simples http請(qǐng)求的測(cè)試代碼
@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)來(lái)自Mockito框架,when表示了執(zhí)行的條件,then用于執(zhí)行驗(yàn)證,這里的操作對(duì)simpleService.findAll方法結(jié)果進(jìn)行了mock,這里 在這一層不需關(guān)心的simpleService的真實(shí)實(shí)現(xiàn)。后面perform方法 mock了 /simples的請(qǐng)求。
這里報(bào)錯(cuò),紅燈,接下來(lái)編寫(xiě)Simple類(lèi)的實(shí)現(xiàn)。
@Entitypublic class Simple { private Long id; private String desc;public Simple(String desc) {this.desc = desc; } }
因?yàn)閟impleService.findAll方法未定義,所以還是報(bào)錯(cuò)的,紅燈。接下來(lái)保持簡(jiǎn)單,給SimpleService創(chuàng)建一個(gè)findAll方法。
public List<Simple> findAll() {return new ArrayList<>(); }
編譯問(wèn)題都解決了,下面開(kāi)始運(yùn)行測(cè)試代碼。
報(bào)錯(cuò),
java.lang.AssertionError: No value at JSON path “$”
還是紅燈,這是因?yàn)槲覀僲ock的perform 沒(méi)有存在。接下來(lái)創(chuàng)建一個(gè)SimpleController類(lèi)作為RestController,并編寫(xiě)/simples請(qǐng)求的接口。
@RestControllerpublic class SimpleController { @Autowired private SimpleService simpleService; @GetMapping('/simples') public ResponseEntity<List<Simple>> getAllSimples() {return new ResponseEntity<>(simpleService.findAll(), HttpStatus.OK); }}
再次運(yùn)行測(cè)試用例,測(cè)試都通過(guò)了,綠燈。
2 Service接下來(lái)讓我們關(guān)注Service層的代碼測(cè)試,test代碼區(qū)創(chuàng)建一個(gè)SimpleServiceTest類(lèi)。該類(lèi)對(duì)下一層Repository依賴(lài),同樣的,創(chuàng)建一個(gè)Repository的mock對(duì)象。
@SpringBootTestpublic class SimpleServiceTest { @MockBean private SimpleRepository simpleRepository;}
編譯報(bào)錯(cuò),紅燈,需要?jiǎng)?chuàng)建一個(gè)SimpleRepository。
@Repositorypublic interface SimpleRepository extends JpaRepository<Simple,Long> {}
以上,創(chuàng)建SimpleRepository作為實(shí)體Simple類(lèi)對(duì)象的JPA存儲(chǔ)服務(wù)。
編寫(xiě)測(cè)試代碼
@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)錯(cuò)的問(wèn)題,SimpleService沒(méi)有構(gòu)造方法。添加Repository 并注入bean。
@Servicepublic class SimpleService { private SimpleRepository simpleRepository; public SimpleService(SimpleRepository simpleRepository) {this.simpleRepository = simpleRepository; } public List<Simple> findAll() {return new ArrayList<>(); }}
這里插播一個(gè)題外話,為啥Spring推薦通過(guò)構(gòu)造方法的方式注入bean, 方便編寫(xiě)可測(cè)試代碼是個(gè)重要原因。
運(yùn)行測(cè)試用例,會(huì)繼續(xù)報(bào)錯(cuò),這里是因?yàn)镴PA hibernate沒(méi)有和實(shí)體類(lèi)對(duì)象交互,需要添加主鍵注解,默認(rèn)構(gòu)造函數(shù) getter/setter 重新編寫(xiě)實(shí)體類(lèi)的代碼。
@Entitypublic 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)行測(cè)試用例 依然失敗,findAll方法測(cè)試未通過(guò),修改SimpleService的findAll方法,調(diào)用 jpa repository的findAll方法
public List<Simple> findAll() {return simpleRepository.findAll(); }
現(xiàn)在再次運(yùn)行測(cè)試用例,測(cè)試通過(guò)。
3 Repository前面已經(jīng)通過(guò)了TDD去實(shí)現(xiàn)Controller層和Service層的代碼,理論上Repository實(shí)現(xiàn)了JPA的接口,我們沒(méi)有做任何代碼的編寫(xiě),應(yīng)該不需要進(jìn)行測(cè)試,但是我們不確定數(shù)據(jù)是否通過(guò)數(shù)據(jù)庫(kù)進(jìn)行了存儲(chǔ)和查詢(xún)。為了保證數(shù)據(jù)庫(kù)存儲(chǔ),將真正的JPA respoitory實(shí)例注入的Service對(duì)象中。修改@MockBean 為@Autowired。
@SpringBootTestpublic 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語(yǔ)句位置spring.datasource.schema=classpath:schema.sql#插入數(shù)據(jù)的MySql語(yǔ)句的位置spring.datasource.data=classpath:data.sql# 禁止自動(dòng)根據(jù)entity創(chuàng)建表結(jié)構(gòu),表結(jié)構(gòu)由schema.sql控制spring.jpa.hibernate.ddl-auto=nonespring.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)行測(cè)試用例,所有用例都測(cè)試通過(guò),瀏覽器直接訪問(wèn)localhost:8080/simples
返回data.sql插入的數(shù)據(jù)
[ {'id': 1,'desc': 'test1'},{'id': 2,'desc': 'test2'}]4 總結(jié)
以上是一個(gè)完整的TDD開(kāi)發(fā)流程的演示,每一個(gè)模塊的測(cè)試具備獨(dú)立性,當(dāng)前模塊中,可以mock其他模塊的數(shù)據(jù)。關(guān)于測(cè)試用例的結(jié)構(gòu),遵循的是AAA模式。
Arrange: 單元測(cè)試的第一步,需要進(jìn)行必要的測(cè)試設(shè)置,譬如創(chuàng)建目標(biāo)類(lèi)對(duì)象,必要時(shí),創(chuàng)建mock對(duì)象和其他變量初始化等等 Action: 調(diào)用要測(cè)試的目標(biāo)方法 Assert: 單元測(cè)試的最后異步,檢查并驗(yàn)證結(jié)果與預(yù)期的結(jié)果是否一致。以上就是Spring Boot web項(xiàng)目的TDD流程的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot web項(xiàng)目TDD的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. idea設(shè)置提示不區(qū)分大小寫(xiě)的方法2. 使用AJAX(包含正則表達(dá)式)驗(yàn)證用戶(hù)登錄的步驟3. Java PreparedStatement用法詳解4. Java實(shí)現(xiàn)的迷宮游戲5. django queryset相加和篩選教程6. IDEA 2020.1.2 安裝教程附破解教程詳解7. Java利用TCP協(xié)議實(shí)現(xiàn)客戶(hù)端與服務(wù)器通信(附通信源碼)8. JS圖片懶加載庫(kù)VueLazyLoad詳解9. 利用ajax+php實(shí)現(xiàn)商品價(jià)格計(jì)算10. Spring如何集成ibatis項(xiàng)目并實(shí)現(xiàn)dao層基類(lèi)封裝

網(wǎng)公網(wǎng)安備