从单元测试到TDD

某次面试被问 “有用过TDD吗?”,现场顿时一排乌鸦飞过~~于是,突发奇想:

TDD定义

1. TDD是什么?

TDD: Test-driven development is a software development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases.

以上一段来自Wiki关于 TDD 的定义,从字面上来看,就是测试驱动开发(先写一系列测试用例,再根据测试用例来写代码)吧。

2. TDD的开发过程是怎样的?

  1. Add a test (写测试用例)
  2. Run all tests. The new test should fail for expected reasons (运行测试用例:这时候大几率会失败,因为我们代码都没写呀)
  3. Write the simplest code that passes the new test (先写一点最简单的代码逻辑,保证测试用例通过吧)
  4. All tests should now pass (这下子测试用例通过了!)
  5. Refactor as needed, using tests after each refactor to ensure that functionality is preserved(然后可以开始优化代码了,但是每次改完都要记得运行测试用例)
  6. Repeat (追求完就重复以上步骤吧)

个人认为,有几个关键点:

  1. 测试用例很关键,最好全面覆盖需求,如果测试用例写得过份简单,测试结果通过了,但是豪无意义(写个“如果1+1应该返回2的测试用例”?)
  2. 写完代码,测试用例通过了,但是会不会有一些代码超出了测试用例范围?(用例写的“1+1=2”,但是写代码的时候还考虑到"1+2=3"啊~~)所以,这里还有个覆盖率的问题。

了解了TDD的概念,理一下涉及的技术点:

  • 单元测试(Unit Test)
  • 代码覆盖率(Code Coverage)

TDD实践

翻了不少现有的文章,这个例子比较简单实用,实现一个自定义的List,可以对照ArrayList跟着上面5个步骤不断优化。
参考例子: Baeldung - How to TDD a List Implementation in Java

环境准备

* 环境:Java 8, Spring 5, Spring Boot 2, Maven
* 开发工具:IntelliJ IDEA 2022.1 社区版

创建Spring项目

  1. IDEA已经集成了Coverage插件,可以用来统计覆盖率,可以根据官方文档设置:IDEA - Code coverage
  2. 使用Spring Initializr,或直接创建Maven Project,在pom.xml添加相关依赖
    Spring Initializr.png

mvn dependency:tree可以看到,JUnit 5已经被自动引入了

 \- org.springframework.boot:spring-boot-starter-test:jar:2.7.2:test
    +- org.springframework.boot:spring-boot-test:jar:2.7.2:test
    +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.7.2:test
    +- org.assertj:assertj-core:jar:3.22.0:test
    +- org.hamcrest:hamcrest:jar:2.2:test
    +- org.junit.jupiter:junit-jupiter:jar:5.8.2:test
    |  +- org.junit.jupiter:junit-jupiter-api:jar:5.8.2:test
    |  |  +- org.opentest4j:opentest4j:jar:1.2.0:test
    |  |  +- org.junit.platform:junit-platform-commons:jar:1.8.2:test
    |  |  \- org.apiguardian:apiguardian-api:jar:1.1.2:test
    |  +- org.junit.jupiter:junit-jupiter-params:jar:5.8.2:test
    |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.8.2:test
    |     \- org.junit.platform:junit-platform-engine:jar:1.8.2:test
    +- org.mockito:mockito-core:jar:4.5.1:test
    |  +- net.bytebuddy:byte-buddy:jar:1.12.12:test
    |  +- net.bytebuddy:byte-buddy-agent:jar:1.12.12:test
    |  \- org.objenesis:objenesis:jar:3.2:test
    +- org.mockito:mockito-junit-jupiter:jar:4.5.1:test

TDD实践

  1. 创建一个类CustomList,实现List接口
package com.example.tdd.unittest.utils;

public class CustomList<E> implements List<E> {
    private Object[] internal = {};
    // 以下是List的实现方案,先不用写任何逻辑
    @Override
    public int size() {
        return 0;
    }
    // 以下省略...
}
  1. 创建测试类CustomListTest
package com.example.tdd.unittest.utils;

public class CustomListTest {
}

注意文件位置, test文件夹和main文件夹是一样的结构,test类都放在 test文件夹里

 +-unit-test
 |  +- src
 |  |  +- main
 |   |  |  +- java
 |  |  +- test
 |   |  |  +- java
  1. 根据TDD的5个步骤来开始实现CustomList
    Step 1. 写测试用例
package com.example.tdd.unittest.utils;

import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

public class CustomListTest {
    /**
     * 预期:向空的list添加元素,返回True
     */
    @Test
    public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() {
        List<Object> list = new CustomList<>();
        boolean succeeded = list.add(null);
        assertTrue(succeeded);
    }
}

Step 2. 运行测试用例
运行结果失败,因为我们还没实现CustomList的add()方法,默认返回false

image.png

Step 3. 使测试用例通过
如果用最简单的方法使测试用例通过,直接把返回值改成true就可以了,但是因为这是演示,测试用例不可能这么简单的!

我们这里不直接使用运行了,我们使用Code Coverage来运行测试用例


image.png

看看结果


image.png

现在通过了,因为使用Code Coverage来运行,可以看看覆盖率
image.png

Class:只有一个类,所以100%覆盖了
Method:CustomList一共有23个方法,但是只覆盖了add()这1个
Line: 只覆盖了add()的部分
具体参数可以从官司网查看: Code Coverage Report

Step 4. 通过全部测试用例
假如测试用例有多个

package com.example.tdd.unittest.utils;

import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

public class CustomListTest {
    /**
     * 预期:向空的list添加元素,返回True
     */
    @Test
    public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() {
        List<Object> list = new CustomList<>();
        boolean succeeded = list.add(null);
        assertTrue(succeeded);
    }
}

    /**
     * 预期:向list添加多元素,并且使用get()可以返回所有元素
     */
    @Test
    public void givenListWithAnElement_whenAnotherIsAdded_thenGetReturnsBoth() {
        List<Object> list = new CustomList<>();
        list.add("test1");
        Object element1 = list.get(0);
        assertEquals("test1", element1);
    }

运行用例,结果


image.png

一个成功,一个失败了,修改一下add()方法和get()方法

public class CustomList<E> implements List<E> {
    @Override
    public boolean add(E e) {
        internal = new Object[] { e };
        return true;
    }
    @Override
    public E get(int index) {
        return (E) internal[index];
    }
}

再运行一次,可以发再用例都通过了


image.png

因为我们修改过代码,所以代码覆盖率也会发生变化


image.png

Step 5. 重构代码,通过全部测试用例
因为上面都是简单粗暴地写代码,让用例通过,后面可能需要重构代码,每次改完都运行测试用例,以及查看覆盖率

Step 6. 重复以上步骤
上面的测试用例是不是太简单了?覆盖不了每个方法,那么要增删用例,就要重复以上步骤啦~~~

总结

TDD,基于测试用例来进行开发,对我来说是一个新鲜的概念(虽然不确定实践效果,或者会不会增加工作量)。作为CRUE的编程小白,单元测试这块再陌生不过了,但是我觉得这块不应该被忽略。

最后的最后,上面的例子只是最探索了一下TDD概念,并不适用真正的项目。可以想想,目前我们的项目,基于上都是基于多层(Controller - Service - Repository) 结构,还有各种外部依赖,直接跑测试用例怎么办?于是我又发现了Mockito这个东西,接下来准备研究研究~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容