測試驅(qū)動開發(fā)(TDD)的實(shí)踐

??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>

組裝

  1. 我們需要定義自己的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();
    }

}
  1. 編寫測試用例帖族,繼承我們的抽象類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();
    }

}
  1. 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)行試試

  1. Maven運(yùn)行測試用例

    image

    隔得時間有的久(三個月前)竖般,加接口變動涣雕,其中一個測試用例跑失敗了。當(dāng)然啦迄埃,這也展示了Spring Restdocs的另一大特性兑障,對文檔的校驗(yàn),能時刻保證您的文檔與接口字段對應(yīng)逞怨,從而減少因文檔不準(zhǔn)引入錯誤的可能性

  2. 運(yùn)行接口文檔

    image

測試驅(qū)動

以上的步驟叠赦,我們走完了測試環(huán)境的搭建竞漾。但測試驅(qū)動并不是寫完功能代碼編寫測試用例窥翩,而且在開始前(設(shè)計(jì)階段)寇蚊,編寫測試用例,為后續(xù)的開發(fā)提供依據(jù)仗岸,同時接口文檔也需要提前生成為前后端分離開發(fā)提供助力

那扒怖,該怎么做呢?

這時候蚂蕴,我們就需要模擬一個實(shí)現(xiàn)類,大部分情況下是模擬一個Service熔号。這里推薦使用Spring Test的一個工具ReflectionTestUtils鸟整,注入測試實(shí)現(xiàn)類

  1. 先創(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;
    }

    //......
}
  1. 在調(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一些對象

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末落追,一起剝皮案震驚了整個濱河市涯肩,隨后出現(xiàn)的幾起案子雹熬,更是在濱河造成了極大的恐慌谣膳,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈菌,死亡現(xiàn)場離奇詭異花履,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)济瓢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門旺矾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夺克,“玉大人铺纽,你說我怎么就攤上這事〗泼牛” “怎么了其馏?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長饶火。 經(jīng)常有香客問我致扯,道長抖僵,這世上最難降的妖魔是什么缘揪? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蹈垢,結(jié)果婚禮上曹抬,老公的妹妹穿的比我還像新娘。我一直安慰自己堰酿,他們只是感情好张足,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布为牍。 她就那樣靜靜地躺著,像睡著了一般凌那。 火紅的嫁衣襯著肌膚如雪吟逝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天励稳,我揣著相機(jī)與錄音囱井,去河邊找鬼。 笑死新翎,一個胖子當(dāng)著我的面吹牛住练,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亏吝,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔚鸥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了馆类?” 一聲冷哼從身側(cè)響起弹谁,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤僵闯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后社裆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體向图,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榄攀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年檩赢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片偶房。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡棕洋,死狀恐怖乒融,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情愧捕,我是刑警寧澤碟摆,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布典蜕,位于F島的核電站,受9級特大地震影響钢猛,放射性物質(zhì)發(fā)生泄漏轩缤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一壶愤、第九天 我趴在偏房一處隱蔽的房頂上張望征椒。 院中可真熱鬧湃累,春花似錦、人聲如沸蒙秒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至净捅,卻和暖如春辩块,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背国章。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工豆村, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人四啰。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓柑晒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親佛掖。 傳聞我的和親對象是個殘疾皇子涌庭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容