Java 中常見(jiàn)的單元測(cè)試
我們?yōu)槭裁磳?xiě)不好單元測(cè)試
寫(xiě)不好單元測(cè)試的情況有很多茴肥,很多時(shí)候我們也是被需求壓著身不由己的就開(kāi)始 “ 胡編亂寫(xiě)” 了皿哨。甚至有的時(shí)候我們都不知道這個(gè)項(xiàng)目可以運(yùn)行多長(zhǎng)時(shí)間,項(xiàng)目剛發(fā)布完就可能進(jìn)入到另一個(gè)項(xiàng)目的開(kāi)發(fā)周期中竟贯,周而復(fù)始幻赚,更沒(méi)有時(shí)間寫(xiě)單元測(cè)試了。
開(kāi)發(fā)人員有一萬(wàn)種理由不寫(xiě)單元測(cè)試:
- 沒(méi)有充分的時(shí)間:通常項(xiàng)目中迭代周期短佳遂,時(shí)間短任務(wù)重,領(lǐng)導(dǎo)昨天晚上的奇思妙想撒顿,恨不得今早上就能上線丑罪,開(kāi)發(fā)人員疲于應(yīng)付,哪有時(shí)間編寫(xiě)單元測(cè)試??凤壁。
- 需求不確定:對(duì)于需求變化特別大的項(xiàng)目吩屹,今天寫(xiě)的單元測(cè)試,明天就不能用了拧抖,甚至剛寫(xiě)完單元測(cè)試煤搜,需求改了,什么有些是邊開(kāi)發(fā)邊磨合需求唧席,這樣更沒(méi)法提前寫(xiě)好單元測(cè)試或者事后補(bǔ)足擦盾。如果大家已經(jīng)習(xí)慣了天天改需求,誰(shuí)還會(huì)寫(xiě)單元測(cè)試呀 ??淌哟。
- 開(kāi)發(fā)過(guò)分依賴測(cè)試團(tuán)隊(duì):認(rèn)為測(cè)試是測(cè)試團(tuán)隊(duì)的事情迹卢,如果不寫(xiě)兩個(gè) bug,他們的績(jī)效怎么辦 ??徒仓。
- 對(duì)單元測(cè)試的意識(shí)不強(qiáng):又不是不能用腐碱,自己過(guò)了一遍就得了 ??。
- 對(duì)單元測(cè)試沒(méi)有明確的要求掉弛。公司或者 QA 團(tuán)隊(duì)症见,甚至開(kāi)發(fā) Leader 對(duì)于單元測(cè)試沒(méi)有明確的要求,所以不寫(xiě)單元測(cè)試殃饿。(大家都不寫(xiě)谋作,我不能卷死他們呀 ??)
- 缺乏單元測(cè)試必要的技能和工具:大多數(shù)還停留在通過(guò)
main
和System.out
方法來(lái)做測(cè)試,效率不高壁晒,還留下了很多無(wú)用的方法 ??瓷们。
當(dāng)然不只是單元測(cè)試业栅,其實(shí)開(kāi)發(fā)連注釋都不寫(xiě)的 ???????秒咐。
單元測(cè)試的重要性
1. 代碼質(zhì)量
單元測(cè)試提高了代碼的質(zhì)量。在實(shí)際編碼之前編寫(xiě)測(cè)試會(huì)讓你去更多的思考方法或者對(duì)象的邊界碘裕,使您編寫(xiě)更好的代碼携取。
2. 及早發(fā)現(xiàn)軟件缺陷
問(wèn)題是在早期階段發(fā)現(xiàn)的。由于單元測(cè)試是由在集成之前測(cè)試單個(gè)代碼的開(kāi)發(fā)人員執(zhí)行的帮孔,因此可以很早就發(fā)現(xiàn)問(wèn)題雷滋,并且可以在不影響其他代碼的情況下解決問(wèn)題不撑。這既包括開(kāi)發(fā)者實(shí)現(xiàn)中的bug,也包括單元規(guī)范中的缺陷或缺失部分晤斩。
3. 易于重構(gòu)
完善的單元測(cè)試可以驗(yàn)證在重構(gòu)代碼或者更新某些依賴的情況下焕檬,確保整個(gè)系統(tǒng)依然能正常的工作。當(dāng)然如果重構(gòu)已經(jīng)改變?cè)瓉?lái)的整體邏輯澳泵,單元測(cè)試也要跟著改動(dòng)
當(dāng)開(kāi)發(fā)者向軟件添加越來(lái)越多的功能時(shí)实愚,有時(shí)需要更改舊的設(shè)計(jì)和代碼。然而兔辅,更改已經(jīng)測(cè)試過(guò)的代碼既有風(fēng)險(xiǎn)又代價(jià)高昂腊敲。如果我們有適當(dāng)?shù)膯卧獪y(cè)試,那么我們就可以自信地進(jìn)行重構(gòu)维苔。
4. 簡(jiǎn)化調(diào)試過(guò)程
單元測(cè)試有助于簡(jiǎn)化調(diào)試過(guò)程碰辅。如果測(cè)試失敗,那么只需要調(diào)試代碼中的最新更改介时。
5. 提供文檔
單元測(cè)試提供了系統(tǒng)的文檔没宾。希望了解單元提供什么功能以及如何使用它的開(kāi)發(fā)人員可以查看單元測(cè)試,以獲得對(duì)單元接口(API)的基本理解潮尝。
6. 設(shè)計(jì)
編寫(xiě)測(cè)試首先迫使您在編寫(xiě)代碼之前仔細(xì)考慮您的設(shè)計(jì)以及它必須完成的任務(wù)榕吼。這不僅能讓你集中注意力,還能讓你創(chuàng)造更好的設(shè)計(jì)勉失。測(cè)試一段代碼迫使您定義該代碼負(fù)責(zé)什么羹蚣。如果您可以很容易地做到這一點(diǎn),那就意味著代碼的職責(zé)定義良好乱凿,因此它具有很高的內(nèi)聚性顽素。
當(dāng)然有興趣的可以看看「測(cè)試驅(qū)動(dòng)開(kāi)發(fā) TDD」
7. 降低成本
由于bug很早就被發(fā)現(xiàn)了,單元測(cè)試有助于降低bug修復(fù)的成本徒蟆。想象一下在開(kāi)發(fā)的后期階段胁出,比如在系統(tǒng)測(cè)試或驗(yàn)收測(cè)試期間發(fā)現(xiàn)的bug的成本。當(dāng)然段审,較早檢測(cè)到的bug更容易修復(fù)全蝶,因?yàn)樯院髾z測(cè)到的bug通常是許多更改的結(jié)果,并且您不知道是哪一個(gè)導(dǎo)致了bug寺枉。
如何寫(xiě)單元測(cè)試
上面講了這么多啰里啰嗦的問(wèn)題抑淫,那我們應(yīng)該怎么寫(xiě)呢?首先我們要明確我們寫(xiě)單元測(cè)試的目的和原則:
目的
- 在開(kāi)發(fā)階段提前減少 Bug
- 提高單元測(cè)試覆蓋率
- 在重構(gòu)時(shí)候姥闪,可以進(jìn)行驗(yàn)證測(cè)試
原則
- 獨(dú)立(可獨(dú)立運(yùn)行始苇,不影響業(yè)務(wù),且不要依賴于第三方服務(wù)的結(jié)果)
- 可重復(fù)(多次測(cè)試筐喳,結(jié)果是一樣的)
- 自動(dòng)化(總不能運(yùn)行一次催式,改一次代碼吧)
- 有明確預(yù)期(根據(jù)傳參知道結(jié)果函喉,總不能單元測(cè)試測(cè)試隨機(jī)數(shù))
一些技巧(讓我們開(kāi)始寫(xiě)單測(cè)吧 ??)
注意: 以下代碼使用 Java 8 和 Maven 環(huán)境下運(yùn)行,其他環(huán)境不保證不出錯(cuò)
放棄寫(xiě) main 和 sysout 吧 ??
比如我們寫(xiě)了一個(gè)工具類(為了展示方便荣月,刪除了具體的實(shí)現(xiàn))管呵,這是幾個(gè)比較常用的
package com.example.ut.util;
import java.util.Objects;
public final class StringUtil {
private StringUtil() {}
public static String firstNonBlank(String... params) {}
public static String firstNonNull(String... params) {}
public static boolean isNullOrEmpty(String string) {}
public static boolean isBlank(String string) {}
public static boolean hasText(String string) {}
public static boolean hasLength(String string) {}
public static String commonPrefix(CharSequence a, CharSequence b) {}
public static String commonSuffix(CharSequence a, CharSequence b) {}
public static String lenientFormat(String template, Object... args) {}
}
比如我們可以看到很多通過(guò)直接在 StringUtil 里面通過(guò) main 方法來(lái)測(cè)試一下各個(gè)方法能不能用,比如這樣:
public final class StringUtil {
public static void main(String[] args) {
System.out.println(firstNonBlank(null, "", "b", "", "d"));
}
...
}
這樣的測(cè)試有意義嗎哺窄?或許當(dāng)時(shí)寫(xiě)代碼的時(shí)候確實(shí)可以用撇寞,但是如何檢驗(yàn)正確性呢?如果重構(gòu)的時(shí)候堂氯,如果發(fā)現(xiàn)已經(jīng)和原來(lái)的行為不一致了呢蔑担?
使用 JUnit5 來(lái)進(jìn)行簡(jiǎn)單的測(cè)試
What is JUnit 5?
Unlike previous versions of JUnit, JUnit 5 is composed of several different modules from three different sub-projects.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
The JUnit Platform serves as a foundation for launching testing frameworks on the JVM. It also defines the TestEngine
API for developing a testing framework that runs on the platform. Furthermore, the platform provides a Console Launcher to launch the platform from the command line and a JUnit 4 based Runner for running any TestEngine
on the platform in a JUnit 4 based environment. First-class support for the JUnit Platform also exists in popular IDEs (see IntelliJ IDEA, Eclipse, NetBeans, and Visual Studio Code) and build tools (see Gradle, Maven, and Ant).
JUnit Jupiter is the combination of the new programming model and extension model for writing tests and extensions in JUnit 5. The Jupiter sub-project provides a TestEngine
for running Jupiter based tests on the platform.
JUnit Vintage provides a TestEngine
for running JUnit 3 and JUnit 4 based tests on the platform.
JUnit 是一個(gè)在 Java 比較基礎(chǔ)的單元測(cè)試框架,主要為了單元測(cè)試而生咽白,現(xiàn)在已經(jīng)到了 JUnit 5, 這里也主要使用 JUnit 5啤握,而不是 JUnit 4。
第一步:引入依賴
這里的版本隨意晶框,能用就行
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.3</version>
<scope>test</scope>
</dependency>
第二步:生成測(cè)試代碼
在 IDEA 中排抬,如果要為某個(gè)類或者方法寫(xiě)單元測(cè)試很簡(jiǎn)單,直接在指定的類或者方法 ctrl + enter
, 即可彈出生成代碼的快捷提示授段,選擇 Test 即可蹲蒲,這里選擇 firstNonNull,hasText侵贵,commonPrefix 來(lái)測(cè)試一下届搁。
自動(dòng)生成的代碼如下(如果你熟悉了就可以自己手寫(xiě),但是 IDEA 能生成窍育,我就不手寫(xiě)了)卡睦,被標(biāo)記 @Test 的方法可以單獨(dú)測(cè)試執(zhí)行,如果你在 IDEA 上可以看到側(cè)邊欄有綠色的帶箭頭的小圓圈漱抓,你可以點(diǎn)擊對(duì)應(yīng)的執(zhí)行 run 或者 debug
import org.junit.jupiter.api.Test;
class StringUtilTest {
@Test
void firstNonBlank() {}
@Test
void hasText() {}
@Test
void commonPrefix() {}
}
第三步:使用 JUnit 5 寫(xiě)一些代碼
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class StringUtilTest {
@Test
void firstNonBlank() {
// 調(diào)用方法得到第一個(gè)非空的字符串表锻,這里應(yīng)該 a
String shouldIsA = StringUtil.firstNonBlank("", null, "a", "c");
// 通過(guò)斷言類來(lái)判定結(jié)果
Assertions.assertEquals("a", shouldIsA);
String shouldIsC = StringUtil.firstNonBlank("c", null, "a", "c");
Assertions.assertEquals("c", shouldIsC);
}
// 可以使用 DisplayName 來(lái)修改原型單元測(cè)試時(shí)的項(xiàng)目名稱
@DisplayName("測(cè)試字符串是不是有文本,空白字符串不認(rèn)為有文本")
@Test
void hasText() {
// 這里應(yīng)該是 false, 因?yàn)?null 沒(méi)有內(nèi)容
Assertions.assertFalse(StringUtil.hasText(null));
// 這里應(yīng)該是 false, 因?yàn)?空字符串 沒(méi)有內(nèi)容
Assertions.assertFalse(StringUtil.hasText(""));
// 這里應(yīng)該是 false, 因?yàn)?空白字符串 沒(méi)有內(nèi)容
Assertions.assertFalse(StringUtil.hasText(" "));
// 這里應(yīng)該是 true, 因?yàn)?a 沒(méi)有內(nèi)容
Assertions.assertTrue(StringUtil.hasText(" a "));
}
@DisplayName("測(cè)試公共前綴")
@Test
void commonPrefix() {
// 無(wú)公共前綴
Assertions.assertEquals("", StringUtil.commonPrefix(" a ", "b"));
Assertions.assertEquals(" ", StringUtil.commonPrefix(" a ", " b"));
Assertions.assertEquals("abab", StringUtil.commonPrefix("ababa", "ababc"));
Assertions.assertNotEquals("aba", StringUtil.commonPrefix("ababa", "ababc"));
}
}
在這里可以點(diǎn) class 上的綠色按鈕來(lái)運(yùn)行下面的全部測(cè)試乞娄,也可以選擇指定的進(jìn)行測(cè)試瞬逊。
這樣一個(gè)最簡(jiǎn)單的單元測(cè)試就完成了,里面用到了: @Test
(必需) 標(biāo)記這是一個(gè)需要測(cè)試的方法仪或;@DispalyName
(可選)為測(cè)試方法或者類起一個(gè)好看的名字或者描述确镊;Assertions
通過(guò)一系列的斷言來(lái)判定結(jié)果是否正確,這步寫(xiě)不寫(xiě)代碼都能通過(guò)溶其,但是應(yīng)該必須寫(xiě)骚腥,否則和 sout 有什么區(qū)別呢敦间?
通過(guò)這三個(gè)的組合使用就能完成一系列的簡(jiǎn)單的單元測(cè)試瓶逃,下面來(lái)看下 Assertions
具體支持什么判定操作束铭。其提供了 282 個(gè)方法,其中大部分有重載厢绝,這里不再展示所有的重載方法契沫,重載的方法只取最大的那個(gè)展示一下
一下內(nèi)容來(lái)自于 org.junit.jupiter.api.Assertions 類中方法
參數(shù)說(shuō)明:message 失敗后提示的信息;expected 預(yù)期的結(jié)果昔汉;actual 實(shí)際的結(jié)果懈万;
代碼實(shí)現(xiàn)其實(shí)是只要 expected 和 actual 不相等就拋異常
方法簽名 | 描述 | 用途 |
---|---|---|
fail(String message, Object expected, Object actual) | 直接調(diào)用,標(biāo)識(shí)一個(gè)測(cè)試用例失敗 | |
assertTrue(boolean condition, String message) | 判定一個(gè)結(jié)果必須是 true | |
assertFalse(boolean condition, String message) | 判定一個(gè)結(jié)果必須是 false | |
assertNull(Object actual, String message) | 結(jié)果不能為 null | |
assertEquals(Object expected, Object actual, String message) | 實(shí)際結(jié)果必須和預(yù)期結(jié)果相等 | |
assertNotEquals(Object expected, Object actual, String message) | 實(shí)際結(jié)果必須和預(yù)期結(jié)果不相等 | |
assertArrayEquals(Object[] expected, Object[] actual, Supplier<String> messageSupplier) | 兩個(gè)數(shù)組必須相等 | |
assertIterableEquals(Iterable<?> expected, Iterable<?> actual, String message) | 兩個(gè)迭代器必須相等 | |
assertSame(Object expected, Object actual, String message) | 實(shí)際結(jié)果必須和預(yù)期結(jié)果是同一個(gè)對(duì)象 | 比如單例的測(cè)試 |
assertNotSame(Object expected, Object actual, String message) | 實(shí)際結(jié)果必須和預(yù)期結(jié)果不是同一個(gè)對(duì)象 | 比如多例的測(cè)試 |
assertAll(Executable... executables) | 所有的 Executable 都執(zhí)行且不拋出異常 | |
assertThrows(Class<T> expectedType, Executable executable, String message) | 必須拋出異常 | |
assertDoesNotThrow(Executable executable, String message) | 不能拋出異常 | |
assertTimeout(Duration timeout, Executable executable, String message) | 指定執(zhí)行時(shí)間內(nèi)執(zhí)行完靶病,Executable 和調(diào)用者在同一個(gè)線程執(zhí)行 | 方法時(shí)長(zhǎng)的判斷 |
assertTimeoutPreemptively(Duration timeout, Executable executable, String message) | 指定執(zhí)行時(shí)間內(nèi)執(zhí)行完会通,Executable 在新的線程執(zhí)行 | 方法時(shí)長(zhǎng)的判斷 |
assertLinesMatch(List<String> expectedLines, List<String> actualLines, String message) | 對(duì)應(yīng)行正則匹配相等,講解麻煩娄周,建議看代碼涕侈,或者單獨(dú)拿出一部分來(lái)講 | |
在上面的例子中,使用了 assertEquals
煤辨、assertFalse
裳涛、assertTrue
、assertNotEquals
的使用众辨,其他的也可以各自嘗試一下端三,使用方法相同。
常見(jiàn)工具
- JUnit
- Mockito
- Assertj
- Hamrest
- 結(jié)合 Spring 的 ut
- Mock 對(duì)象
- DB