junit5
JUnit5在2017年就發(fā)布了霹娄,你還在用junit4嗎?
什么是junit5
與以前的JUnit版本不同,JUnit 5由三個(gè)不同子項(xiàng)目的多個(gè)不同模塊組成犬耻。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform為在JVM上啟動(dòng)測(cè)試框架提供基礎(chǔ)踩晶。它還定義了TestEngine API, 用來開發(fā)在平臺(tái)上運(yùn)行的測(cè)試框架。此外枕磁,平臺(tái)提供了一個(gè)控制臺(tái)啟動(dòng)器]合瓢,用于從命令行啟動(dòng)平臺(tái),并為Gradle和Maven提供構(gòu)建插件以[基于JUnit 4的Runner透典,用于在平臺(tái)上運(yùn)行任意TestEngine
晴楔。
JUnit Jupiter是在JUnit 5中編寫測(cè)試和擴(kuò)展的新型編程模型和[擴(kuò)展模型][]的組合.Jupiter子項(xiàng)目提供了TestEngine
,用于在平臺(tái)上運(yùn)行基于Jupiter的測(cè)試峭咒。
JUnit Vintage提供TestEngine
税弃,用于在平臺(tái)上運(yùn)行基于JUnit 3和JUnit 4的測(cè)試。
為什么需要 JUnit 5
自從有了類似 JUnit 之類的測(cè)試框架凑队,Java 單元測(cè)試領(lǐng)域逐漸成熟则果,開發(fā)人員對(duì)單元測(cè)試框架也有了更高的要求:更多的測(cè)試方式,更少的其他庫的依賴漩氨。
因此西壮,大家期待著一個(gè)更強(qiáng)大的測(cè)試框架誕生,JUnit 作為Java測(cè)試領(lǐng)域的領(lǐng)頭羊叫惊,推出了 JUnit 5 這個(gè)版本款青,主要特性:
- 提供全新的斷言和測(cè)試注解,支持測(cè)試類內(nèi)嵌
- 更豐富的測(cè)試方式:支持動(dòng)態(tài)測(cè)試霍狰,重復(fù)測(cè)試抡草,參數(shù)化測(cè)試等
- 實(shí)現(xiàn)了模塊化,讓測(cè)試執(zhí)行和測(cè)試發(fā)現(xiàn)等不同模塊解耦蔗坯,減少依賴
- 提供對(duì) Java 8 的支持康震,如 Lambda 表達(dá)式,Sream API等宾濒。
基本注解
@Test: 表示方法是測(cè)試方法腿短。但是與JUnit4的@Test不同,他的職責(zé)非常單一不能聲明任何屬性绘梦,拓展的測(cè)試將會(huì)由Jupiter提供額外測(cè)試
@ParameterizedTest: 表示方法是參數(shù)化測(cè)試
@RepeatedTest: 表示方法可重復(fù)執(zhí)行
@DisplayName: 為測(cè)試類或者測(cè)試方法設(shè)置展示名稱
@BeforeEach: 表示在每個(gè)單元測(cè)試之前執(zhí)行
@AfterEach: 表示在每個(gè)單元測(cè)試之后執(zhí)行
@BeforeAll: 表示在所有單元測(cè)試之前執(zhí)行
@AfterAll: 表示在所有單元測(cè)試之后執(zhí)行
@Tag: 表示單元測(cè)試類別橘忱,類似于JUnit4中的@Categories
@Disabled: 表示測(cè)試類或測(cè)試方法不執(zhí)行,類似于JUnit4中的@Ignore
@Timeout: 表示測(cè)試方法運(yùn)行如果超過了指定時(shí)間將會(huì)返回錯(cuò)誤
@ExtendWith: 為測(cè)試類或測(cè)試方法提供擴(kuò)展類引用
常用注解格式:
class StandardTests {
//與junit4的@beforeClass類似谚咬,每個(gè)測(cè)試類運(yùn)行一次
@BeforeAll
static void initAll() {
}
//與junit4中@before類似鹦付,每個(gè)測(cè)試用例都運(yùn)行一次
@BeforeEach
void init() {
}
@Test
@DisplayName("成功測(cè)試")
void succeedingTest() {
}
@Test
@DisplayName("失敗測(cè)試")
void failingTest() {
fail("a failing test");
}
//禁用測(cè)試用例
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
//與@BeforeEach對(duì)應(yīng)尚粘,每個(gè)測(cè)試類執(zhí)行一次择卦,一般用于恢復(fù)環(huán)境
@AfterEach
void tearDown() {
}
//與@BeforeAll對(duì)應(yīng),每個(gè)測(cè)試類執(zhí)行一次,一般用于恢復(fù)環(huán)境
@AfterAll
static void tearDownAll() {
}
}
新特性
顯示名稱
@DisplayName("顯示名稱測(cè)試")
class DisplayNameDemo {
@Test
@DisplayName("我的 第一個(gè) 測(cè)試 用例")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("??")
void testWithDisplayNameContainingEmoji() {
}
}
IDE運(yùn)行測(cè)試結(jié)果顯示:
優(yōu)點(diǎn):通過這種方式秉继,可以在方法名是英文特別長或者很難用英文描述清楚的場(chǎng)景下祈噪,增加中文解釋
更強(qiáng)大的斷言
JUnit Jupiter提供了許多JUnit4已有的斷言方法,并增加了一些適合與Java 8 lambda一起使用的斷言方法尚辑。所有JUnit Jupiter斷言都是[org.junit.jupiter.Assertions]類中的靜態(tài)方法辑鲤。
分組斷言:
多個(gè)條件同時(shí)滿足時(shí)才斷言成功
@Test
void groupedAssertions() {
Person person = new Person();
Assertions.assertAll("person",
() -> assertEquals("niu", person.getName()),
() -> assertEquals(18, person.getAge())
);
}
異常斷言:
Junit4時(shí)需要使用rule方式,junit5提供了assertThrows更優(yōu)雅的異常斷言
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
超時(shí)斷言:
@Test
@DisplayName("超時(shí)測(cè)試")
public void timeoutTest() {
Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(50));
}
標(biāo)簽和過濾
通過標(biāo)簽把測(cè)試分組杠茬,在不同階段執(zhí)行不同的邏輯測(cè)試月褥,比如劃分為快速冒煙測(cè)試和執(zhí)行慢但也重要的測(cè)試
@Test
@Tag("fast")
void testing_faster() {
}
@Test
@Tag("slow")
void testing_slow() {
}
然后通過配置maven-surefire-plugin插件
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<properties>
<includeTags>fast</includeTags>
<excludeTages>slow</excludeTages>
</properties>
</configuration>
</plugin>
嵌套測(cè)試
當(dāng)我們編寫的類和代碼逐漸增多,隨之而來的需要測(cè)試的對(duì)應(yīng)測(cè)試類也會(huì)越來越多瓢喉。
為了解決測(cè)試類數(shù)量爆炸的問題宁赤,JUnit 5提供了@Nested 注解,能夠以靜態(tài)內(nèi)部成員類的形式對(duì)測(cè)試用例類進(jìn)行邏輯分組栓票。
并且每個(gè)靜態(tài)內(nèi)部類都可以有自己的生命周期方法决左, 這些方法將按從外到內(nèi)層次順序執(zhí)行。
此外走贪,嵌套的類也可以用@DisplayName 標(biāo)記佛猛,這樣我們就可以使用正確的測(cè)試名稱。下面看下簡單的用法:
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
}
}
}
junit沒有限制嵌套層數(shù)坠狡,除非必要一般不建議使用超過3層继找,過于復(fù)雜的層次結(jié)構(gòu)會(huì)增加開發(fā)者理解用例關(guān)系的難度
構(gòu)造函數(shù)和方法的依賴注入
在之前的所有JUnit版本中,測(cè)試構(gòu)造函數(shù)或方法都不允許有參數(shù)(至少不能使用標(biāo)準(zhǔn)的Runner實(shí)現(xiàn))逃沿。作為JUnit Jupiter的主要變化之一码荔,測(cè)試構(gòu)造函數(shù)和方法現(xiàn)在都允許有參數(shù)。這帶來了更大的靈活性感挥,并為構(gòu)造函數(shù)和方法啟用依賴注入
- TestInfo可獲取測(cè)試信息
- TestReporter可以向控制臺(tái)輸出信息
@Test
@DisplayName("test-first")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("test-first", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
@DisplayName("test-second")
@Tag("my-tag")
void test2(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
重復(fù)測(cè)試
多次調(diào)用同一個(gè)測(cè)試用例
@RepeatedTest(10)
@DisplayName("重復(fù)測(cè)試")
public void testRepeated() {
//...
}
動(dòng)態(tài)測(cè)試
動(dòng)態(tài)測(cè)試只需要編寫一處代碼缩搅,就能一次性對(duì)各種類型的輸入和輸出結(jié)果進(jìn)行驗(yàn)證
@TestFactory
@DisplayName("動(dòng)態(tài)測(cè)試")
Stream<DynamicTest> dynamicTests() {
List<Person> persons = getAllPerson();
return persons.stream()
.map(person -> DynamicTest.dynamicTest(person.getName() + "-test", () -> assertTrue(person.getName().contains("niu"))));
}
超時(shí)測(cè)試
通過時(shí)間來驗(yàn)證用例是否超時(shí),一般要求單個(gè)單元測(cè)試不應(yīng)該超過1秒
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 1000, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds1000Milliseconds() {
// fails if execution time exceeds 1000 milliseconds
//也可用這種方式 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1500));
}
}
參數(shù)測(cè)試
參數(shù)測(cè)試我覺得是最好用的特性触幼,可以大量減少重復(fù)模板式代碼硼瓣,也是junit5最驚艷的提升,強(qiáng)烈推薦使用
@ValueSource: 為參數(shù)化測(cè)試指定入?yún)碓粗们С职舜蠡A(chǔ)類以及String類型,Class類型
@NullSource: 表示為參數(shù)化測(cè)試提供一個(gè)null的入?yún)?/p>
@EnumSource: 表示為參數(shù)化測(cè)試提供一個(gè)枚舉入?yún)?/p>
@CsvSource:表示讀取CSV格式內(nèi)容作為參數(shù)化測(cè)試入?yún)?/p>
@CsvFileSource:表示讀取指定CSV文件內(nèi)容作為參數(shù)化測(cè)試入?yún)?/p>
@MethodSource:表示讀取指定方法的返回值作為參數(shù)化測(cè)試入?yún)?注意方法返回需要是一個(gè)流)
@ArgumentsSource:指定一個(gè)自定義的堂鲤,可重用的
ArgumentsProvider
。
看完用法描述媒峡,簡直太喜歡了
一個(gè)頂三個(gè)基礎(chǔ)測(cè)試用例
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("參數(shù)化測(cè)試1")
public void parameterizedTest1(String string) {
assertTrue(StringUtils.isNotBlank(string));
}
如果不是基礎(chǔ)的類型瘟栖,可以使用方法構(gòu)造,只要返回值為Stream類型就可以谅阿,多個(gè)參數(shù)使用Arguments
實(shí)例流
@ParameterizedTest
@MethodSource("method")
@DisplayName("方法來源參數(shù)")
public void testWithExplicitLocalMethodSource(String name) {
Assertions.assertNotNull(name);
}
private static Stream<String> method() {
return Stream.of("apple", "banana");
}
@CsvSource
允許您將參數(shù)列表表示為以逗號(hào)分隔的值(例如半哟,字符串文字)
@ParameterizedTest
@CsvSource({"steven,18", "jack,24"})
@DisplayName("參數(shù)化測(cè)試-csv格式")
public void parameterizedTest3(String name, Integer age) {
System.out.println("name:" + name + ",age:" + age);
Assertions.assertNotNull(name);
Assertions.assertTrue(age > 0);
}
@CsvFileSource
使用classpath中的CSV文件酬滤,CSV文件中的每一行都會(huì)導(dǎo)致參數(shù)化測(cè)試的一次調(diào)用
這種就完全把測(cè)試數(shù)據(jù)與測(cè)試方法隔離,達(dá)到更好解耦效果
@ParameterizedTest
@CsvFileSource(resources = "/persons.csv") //指定csv文件位置
@DisplayName("參數(shù)化測(cè)試-csv文件")
public void parameterizedTest2(String name, Integer age) {
System.out.println("name:" + name + ",age:" + age);
Assertions.assertNotNull(name);
Assertions.assertTrue(age > 0);
}
其他方式不在贅述寓涨,如果還是滿足不了需求盯串,可以通過@ArgumentsSource自定義自己的數(shù)據(jù)來源,必須封裝成去取JSON或者XMl等數(shù)據(jù)
AssertJ
當(dāng)定義好需要運(yùn)行的測(cè)試方法后戒良,下一步則是需要關(guān)注測(cè)試方法的細(xì)節(jié)体捏,這就離不開斷言和假設(shè)
斷言:封裝好了常用判斷邏輯,當(dāng)不滿足條件時(shí)糯崎,該測(cè)試用例會(huì)被認(rèn)為測(cè)試失敗
假設(shè):與斷言類似几缭,當(dāng)條件不滿足時(shí),測(cè)試會(huì)直接退出而不是判定為失敗
因?yàn)椴粫?huì)影響到后續(xù)的測(cè)試用例沃呢,最常用的還是斷言
除了Junit5自帶的斷言奏司,AssertJ是非常好用的一個(gè)斷言工具,最大特點(diǎn)是提供了流式斷言樟插,與Java8使用方法非常類似
@Test
void testString() {
// 斷言null或?yàn)榭兆址? assertThat("").isNullOrEmpty();
// 斷言空字符串
assertThat("").isEmpty();
// 斷言字符串相等 斷言忽略大小寫判斷字符串相等
assertThat("niu").isEqualTo("niu").isEqualToIgnoringCase("NIu");
// 斷言開始字符串 結(jié)束字符穿 字符串長度
assertThat("niu").startsWith("ni").endsWith("u").hasSize(3);
// 斷言包含字符串 不包含字符串
assertThat("niu").contains("iu").doesNotContain("love");
// 斷言字符串只出現(xiàn)過一次
assertThat("niu").containsOnlyOnce("iu");
}
@Test
void testNumber() {
// 斷言相等
assertThat(42).isEqualTo(42);
// 斷言大于 大于等于
assertThat(42).isGreaterThan(38).isGreaterThanOrEqualTo(38);
// 斷言小于 小于等于
assertThat(42).isLessThan(58).isLessThanOrEqualTo(58);
// 斷言0
assertThat(0).isZero();
// 斷言正數(shù) 非負(fù)數(shù)
assertThat(1).isPositive().isNotNegative();
// 斷言負(fù)數(shù) 非正數(shù)
assertThat(-1).isNegative().isNotPositive();
}
@Test
void testCollection() {
// 斷言 列表是空的
assertThat(newArrayList()).isEmpty();
// 斷言 列表的開始 結(jié)束元素
assertThat(newArrayList(1, 2, 3)).startsWith(1).endsWith(3);
// 斷言 列表包含元素 并且是排序的
assertThat(newArrayList(1, 2, 3)).contains(1, atIndex(0)).contains(2, atIndex(1)).contains(3)
.isSorted();
// 斷言 被包含與給定列表
assertThat(newArrayList(3, 1, 2)).isSubsetOf(newArrayList(1, 2, 3, 4));
// 斷言 存在唯一元素
assertThat(newArrayList("a", "b", "c")).containsOnlyOnce("a");
}
@Test
void testMap() {
Map<String, Object> foo = ImmutableMap.of("A", 1, "B", 2, "C", 3);
// 斷言 map 不為空 size
assertThat(foo).isNotEmpty().hasSize(3);
// 斷言 map 包含元素
assertThat(foo).contains(entry("A", 1), entry("B", 2));
// 斷言 map 包含key
assertThat(foo).containsKeys("A", "B", "C");
// 斷言 map 包含value
assertThat(foo).containsValue(3);
}
// 其他斷言韵洋,請(qǐng)自行探索......
想想如果沒有使用AssertJ時(shí)我們是如何寫斷言的,是不是需要多個(gè)assert黄锤,很繁瑣
AssertJ的斷言代碼清爽很多搪缨,流式斷言充分利用了java8之后的匿名方法和stream類型的特點(diǎn),很好的對(duì)Junit斷言方法做了補(bǔ)充鸵熟。
參考