??Mine Site:https://www.dnocm.com/articles/almond/test-driven%20development/
測試驅(qū)動開發(fā)(TDD)是一種很好的方法論,雖然在國內(nèi)并不被重視物蝙。但仍然想抽時間寫一篇關(guān)于測試驅(qū)動開發(fā)的文檔癌蓖。
OK,最好的描述方式應(yīng)該分為三部分吧挨摸,是什么?為什么崩侠?怎么做况脆?那么就從這三部分友扰,分別的描述測試驅(qū)動開發(fā)方法論。
What: TDD 是什么
測試驅(qū)動開發(fā)赚爵,英文全稱Test-Driven Development餐弱,簡稱TDD,是一種不同于傳統(tǒng)軟件開發(fā)流程的新型的開發(fā)方法囱晴。它要求在編寫某個功能的代碼之前先編寫測試代碼膏蚓,然后只編寫使測試通過的功能代碼,通過測試來推動整個開發(fā)的進(jìn)行畸写。這有助于編寫簡潔可用和高質(zhì)量的代碼驮瞧,并加速開發(fā)過程。
Kent Beck先生最早在其極限編程(XP)方法論中论笔,向大家推薦“測試驅(qū)動”這一最佳實(shí)踐,還專門撰寫了《測試驅(qū)動開發(fā)》一書狂魔,詳細(xì)說明如何實(shí)現(xiàn)。經(jīng)過幾年的迅猛發(fā)展最楷,測試驅(qū)動開發(fā)已經(jīng)成長為一門獨(dú)立的軟件開發(fā)技術(shù)整份,其名氣甚至蓋過了極限編程籽孙。
Why: 為什么需要 TDD
再摘個百度百科中的例子
蓋房子的時候,工人師傅砌墻犯建,會先用樁子拉上線讲冠,以使磚能夠壘的筆直,因?yàn)閴敬u的時候都是以這根線為基準(zhǔn)的适瓦。TDD就像這樣,先寫測試代碼德迹,就像工人師傅先用樁子拉上線揭芍,然后編碼的時候以此為基準(zhǔn),只編寫符合這個測試的功能代碼肌毅。
而一個新手或菜鳥級的小師傅姑原,卻可能不知道拉線,而是直接把磚往上壘锭汛,壘了一些之后再看是否筆直,這時候可能會用一根線唤殴,量一下砌好的墻是否筆直朵逝,如果不直再進(jìn)行校正,敲敲打打啤咽。使用傳統(tǒng)的軟件開發(fā)過程就像這樣,我們先編碼瓶佳,編碼完成之后才寫測試程序鳞青,以此檢驗(yàn)已寫的代碼是否正確,如果有錯誤再一點(diǎn)點(diǎn)修改。
你是希望先砌墻再拉線潜腻,還是希望先拉線再砌墻呢?如果你喜歡前者童番,那就算了威鹿,而如果你喜歡后者忽你,那就轉(zhuǎn)入TDD陣營吧!詳細(xì)可參閱根蟹。
上述例子中也已經(jīng)能看出TDD的優(yōu)點(diǎn)糟秘。但還是做個簡單總結(jié)吧
它有助于編寫簡潔可用和高質(zhì)量的代碼,有很高的靈活性和健壯性散庶,能快速響應(yīng)變化凌净,并加速開發(fā)過程
我們可以這么理解這句話,原本需求->產(chǎn)品設(shè)計(jì)->產(chǎn)品實(shí)現(xiàn)躲舌,調(diào)整為需求->產(chǎn)品設(shè)計(jì)->產(chǎn)品開發(fā)設(shè)計(jì)(Test階段)->產(chǎn)品實(shí)現(xiàn)(Develop階段)
- 產(chǎn)品開發(fā)設(shè)計(jì)(Test過程): 由于僅先編寫測試用例性雄,相對于直接的開發(fā)更加迅速,能快速的響應(yīng)需求的變化
- 產(chǎn)品實(shí)現(xiàn)(Develop階段): 我們僅需確保測試用例都通過诀拭,能有效的降低引入bug的可能性煤蚌。同時測試用例的存在,對于后期維護(hù)筒占,提供了強(qiáng)大的支持(回歸測試)
How: TDD 如何實(shí)踐
我的實(shí)踐是 Spring Test + TestNG 集成測試蜘犁,再配合 Spring Restdocs 文檔生成。
Spring Test
首先奏窑,這不是一個獨(dú)立的框架屈扎,它與Spring框架是綁在一起的鹰晨,正如開頭的第一句話所說,測試驅(qū)動在國內(nèi)不受重視巍实,但在國外恰恰相反哩牍。大部分國外的開源框架都集成了測試所需的一些工具類,比如Spring Boot 單獨(dú)的一節(jié)講解測試丸边。在這里我們需要用到它的一個TestNG支持的抽象類AbstractTransactionalTestNGSpringContextTests
荚孵,這個類的用于初始化Spring環(huán)境以及添加事務(wù)支持
TestNG
在Java里收叶,最為流行的測試框架應(yīng)該是JUnit和TestNG,他們的功能也十分相似蜓萄。在這里,做個簡單的比較辟犀,和闡述一下采用TestNG的原因
首先绸硕,先說一下JUnit,它是個優(yōu)秀的單元測試框架出嘹,嚴(yán)格的遵守一個實(shí)現(xiàn)類一個測試類的方式咬崔。事實(shí)上刁赦,如果對代碼質(zhì)量要求很高闻镶,的確需要對每個類都編寫測試用例。但例如Spring的代碼牺氨,分為Dao層墩剖,Service層岭皂,Controller層,即便只是完成一個小功能书劝,都需要編寫多個測試類土至,來完成測試。這中間會耗費(fèi)許多的時間骡苞,同時對于我們程序猿來說,也是件痛苦的事贴见。而且蝇刀,一般情況下徘溢,并需要如此高的質(zhì)量。TestNG既包涵了JUnit的單元測試的功能站粟,同時他也可以進(jìn)行集成測試曾雕。我們僅需對功能點(diǎn)(接口)編寫相應(yīng)的集成測試剖张,這能減少大量的代碼量。所以幅虑,如果能把測試用例的編寫變成一般輕松的事顾犹,誰不愿這么做呢
Spring Restdocs
Spring REST Docs helps you to document RESTful services. It combines hand-written documentation written with Asciidoctor and auto-generated snippets produced with Spring MVC Test. This approach frees you from the limitations of the documentation produced by tools like Swagger. It helps you to produce documentation that is accurate, concise, and well-structured. This documentation then allows your users to get the information they need with a minimum of fuss.
簡單的說炫刷,它能使用Asciidoctor組合Spring MVC Test生成的代碼片段,編寫RESTful的接口文檔
環(huán)境配置
主要是Maven的配置绍申,因?yàn)槭褂肨estNG以及Spring Restdocs顾彰,我們需要添加以下依賴
<!-- test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<!-- option: remove junit -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- testng -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- restdocs -->
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
同時還需要配置Maven插件
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<configuration>
<!-- 默認(rèn)位置在src/main/asciidoc下 -->
<sourceDocumentName>index.adoc</sourceDocumentName>
<doctype>book</doctype>
<attributes>
<allow-uri-read>true</allow-uri-read>
<attribute-missing>warn</attribute-missing>
</attributes>
</configuration>
<executions>
<execution>
<id>generate-docs</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
<sourceHighlighter>highlight.js</sourceHighlighter>
<attributes>
<toc2 />
<docinfo>shared-head</docinfo>
</attributes>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
</plugin>
組裝
- 我們需要定義自己的TestNG抽象類,繼承
AbstractTransactionalTestNGSpringContextTests
拆又,并配置Spring Restdocs
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AbstractAssetsTests extends AbstractTransactionalTestNGSpringContextTests {
private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("target/generated-snippets");
@Autowired
private WebApplicationContext context;
protected MockMvc mockMvc;
@BeforeMethod
public void setUp(Method method) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(documentationConfiguration(this.restDocumentation)).build();
this.restDocumentation.beforeTest(getClass(), method.getName());
}
@AfterMethod
public void tearDown() {
this.restDocumentation.afterTest();
}
}
- 編寫測試用例帖族,繼承我們的抽象類
AbstractAssetsTests
public class UserControllerTest extends AbstractAssetsTests {
@Resource
private UserService userService;
@Test
@Rollback
public void add() throws Exception {
User user = getMockUser();
super.mockMvc.perform(MockMvcRequestBuilders.post("/user/add")
.contentType(MediaType.APPLICATION_JSON)
.content(Objects.requireNonNull(JacksonUtils.toJson(user))))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(document("user-add"));
}
@Test
@Rollback
public void delete() throws Exception {
ResultDto<User> add = userService.add(getMockUser());
User user = add.getObject();
super.mockMvc.perform(MockMvcRequestBuilders.delete("/user/delete")
.param("ids",user.getId()+""))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(document("user-delete"));
}
//......
private User getMockUser() {
return User.builder()
.name("test-001")
.password("123456")
.pointId(1L)
.roleId(1L)
.description("TestNG測試帳號")
.build();
}
}
- Asciidoctor拼接代碼片段
= 接口文檔
Mr.J;
:toc2:
:toc-title: 目錄
:doctype: book
:icons: font
:source-highlighter: highlightjs
:docinfo: shared-head
include::readme.adoc[]
include::user/user-list.adoc[]
== 例子
簡單的接口文檔使用 Spring REST Docs 和 TestNG.
`SampleTestNgApplicationTests` makes a call to a very simple service and produces three
documentation snippets.
用戶添加:
include::{snippets}/user-add/curl-request.adoc[]
用戶添加響應(yīng):
include::{snippets}/user-add/http-response.adoc[]
=== 三級標(biāo)題
恩恩恩
運(yùn)行試試
-
Maven運(yùn)行測試用例
image
隔得時間有的久(三個月前)竖般,加接口變動涣雕,其中一個測試用例跑失敗了。當(dāng)然啦迄埃,這也展示了Spring Restdocs的另一大特性兑障,對文檔的校驗(yàn),能時刻保證您的文檔與接口字段對應(yīng)逞怨,從而減少因文檔不準(zhǔn)引入錯誤的可能性 -
運(yùn)行接口文檔
image
測試驅(qū)動
以上的步驟叠赦,我們走完了測試環(huán)境的搭建竞漾。但測試驅(qū)動并不是寫完功能代碼編寫測試用例窥翩,而且在開始前(設(shè)計(jì)階段)寇蚊,編寫測試用例,為后續(xù)的開發(fā)提供依據(jù)仗岸,同時接口文檔也需要提前生成為前后端分離開發(fā)提供助力
那扒怖,該怎么做呢?
這時候蚂蕴,我們就需要模擬一個實(shí)現(xiàn)類,大部分情況下是模擬一個Service熔号。這里推薦使用Spring Test的一個工具ReflectionTestUtils
鸟整,注入測試實(shí)現(xiàn)類
- 先創(chuàng)建service接口的測試實(shí)現(xiàn),例如
public class UserServiceTestBean implements UserService {
@Override
public ResultDto<User> getUserById(long id) {
ResultDto<User> result = new ResultDto<>(ResultCode.SUCCESS);
result.setObject(new User());
return result;
}
@Override
public ResultDto<User> add(User t) {
ResultDto<User> result = new ResultDto<>(ResultCode.SUCCESS);
result.setObject(t);
return result;
}
//......
}
- 在調(diào)用之前注入測試的模擬對象
@Test
@Rollback
public void add() throws Exception {
//為userController注入userService對象
ReflectionTestUtils.setField(userController, "userService", new UserServiceTestBean());
User user = getMockUser();
super.mockMvc.perform(MockMvcRequestBuilders.post("/user/add")
.contentType(MediaType.APPLICATION_JSON)
.content(Objects.requireNonNull(JacksonUtils.toJson(user))))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(document("user-add"));
}
這樣我們完成了在實(shí)現(xiàn)之前,優(yōu)先編寫完測試用例亮瓷。當(dāng)然當(dāng)service實(shí)現(xiàn)后降瞳,相應(yīng)的mock代碼都需要注釋掉。使用Mockito模擬service對象也是行的除师,但在嘗試后扔枫,不如直接編寫測試對象來的高效。
結(jié)尾
上面代碼開源在GitHub上倚舀,有興趣的可以去看看
https://github.com/JiangTJ/enterpriseAssetManagement/tree/testng&spring-rest-docs
缺少mock相關(guān)的代碼痕貌,畢竟當(dāng)時寫測試用例時糠排,service已經(jīng)全部實(shí)現(xiàn)了,當(dāng)然哺徊,您可以fork后自己嘗試一下mock一些對象