概述
JUnit 是Java生态系统中最受欢迎的单元测试框架之一。
Maven依赖关系
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
该版本需要Java8才能工作。
架构
- JUnit平台
- JUnit Jupiter
- JUnit Vintage
JUint平台
该平台负责在JVM上启动测试框架,它在JUnit及其客户端(如构建工具)之间定义了一个稳定而强大的接口。该平台可轻松将客户端与JUint集成,以发现和执行测试。它还定义了TestEngineAPI,用于开发在JUnit平台上运行的测试框架。通过实现自定义TestEngine,我们可以将第三方测试库直接插入JUnit。
JUnit Jupiter
此模块包括用于在JUnit5中编写测试的新编程和扩展模型。与JUnit4相比,新的注解包括:
注解 | 含义 |
---|---|
@TestFactory | 表示作为动态测试的测试工厂的方法 |
@DisplayName | 定义测试类或测试方法的自定义显示名称 |
@Nested | 表示带注解的类是嵌套的非静态测试类 |
@Tag | 声明用于过滤测试的标记 |
@ExtendWith | 注册自定义扩展 |
@BeforeEach | 表示带注解的的方法将在每个测试方法执行之前执行(代替@Before) |
@AfterEach | 表示带注解的方法将在每个测试方法执行之后执行(代替@After) |
@BeforeAll | 表示带注解的方法将在该类所有方法执行之前执行(代替@BeforeClass) |
@AfterAll | 表示带注解的方法将在该类所有方法执行之后执行(代替@AfterClass) |
@Disable | 禁用测试类或方法(代替@Ignore) |
JUnit Vintage
JUnit Vintage支持在JUnit5上运行基于JUnit3和JUnit4的测试。
基本注解
@BeforeAll和@BeforeEach
@BeforeAll
static void setup() {
log.info("@BeforeAll - executes once before all test methods in this class");
}
@BeforeEach
void init() {
log.info("@BeforeEach - executes before each test method in this class");
}
具有@BeforeAll注解的方法必须是静态的。
@DisplayName 和Disable
@DisplayName("Single test successful")
@Test
void testSingleSuccessTest() {
log.info("Success");
}
@Test
@Disabled("Not implemented yet")
void testShowSomething() {
}
我们可以使用注解更改显示名称或禁用带有注解的方法。
AfterEach和@AfterAll
@AfterEach
void tearDown() {
log.info("@AfterEach - executed after each test method.");
}
@AfterAll
static void done() {
log.info("@AfterAll - executed after all test methods.");
}
带有@AfterAll注解的方法也必须是静态的。
@Test
为了识别测试,我们将添加@Test注解。我们可以在一个类中拥有任意数量的@Test方法,但是提倡将相关内容放在放在一起。另外注意,测试不能说是私有的,也不能有返回值,否则它将会被忽略。
断言和假设
JUint试图充分利用Java 8的新功能,尤其是Lambda表达式。
断言
断言是支持在测试中断言条件的方法,这些方法可通过JUnit4中的Assert类和JUnit5中的Assertions类进行访问。为了提高测试和断言本身的可读性,始终建议静态导入相应的类。通过这种方式,我们可以引用断言方法本身,而无需将表示类作为前缀。
断言已移至org.junit.jupiter.api.Assertions,我们可以在断言中使用lambda:
@Test
void lambdaExpressions() {
List numbers = Arrays.asList(1, 2, 3);
assertTrue(numbers.stream()
.mapToInt(Integer::intValue)
.sum() > 5, () -> "Sum should be greater than 5");
}
可以使用assertAll()对断言进行分组,使用MultiFailuresError可以报告组内任何失败的断言:
@Test
void groupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
assertAll("numbers",
() -> assertEquals(numbers[0], 1),
() -> assertEquals(numbers[3], 3),
() -> assertEquals(numbers[4], 1)
);
}
JUnit4中的断言
assertEquals断言验证预期值和实际值是否相等:
@Test
public void whenAssertingEquality_thenEqual() {
String expected = "Baeldung";
String actual = "Baeldung";
assertEquals(expected, actual);
}
还可以指定断言失败时要显示的消息:
assertEquals("failure - strings are not equal", expected, actual);
assertArrayEquals断言两个数组是否相等:
@Test
public void whenAssertingArraysEquality_thenEqual() {
char[] expected = {'J','u','n','i','t'};
char[] actual = "Junit".toCharArray();
assertArrayEquals(expected, actual);
}
如果两个数组都为 null,则断言将认为它们相等:
@Test
public void givenNullArrays_whenAssertingArraysEquality_thenEqual() {
int[] expected = null;
int[] actual = null;
assertArrayEquals(expected, actual);
}
assertNotSame可以验证两个变量是否不引用同一对象:
@Test
public void whenAssertingNotSameObject_thenDifferent() {
Object cat = new Object();
Object dog = new Object();
assertNotSame(cat, dog);
}
相反我们想断言两个变量是引用同一对象时,可以使用assertSame断言。
- ###assertTrue和assertFalse
想验证某个条件是真的还是假的,可以使用assertTrue断言或assertFalse断言:
@Test
public void whenAssertingConditions_thenVerified() {
assertTrue("5 is greater then 4", 5 > 4);
assertFalse("5 is not greater then 6", 5 > 6);
}
fail断言未通过引发失败错误的测试。它可用于验证是否抛出实际异常。
@Test
public void whenCheckingExceptionMessage_thenEqual() {
try {
methodThatShouldThrowException();
fail("Exception not thrown");
} catch (UnsupportedOperationException e) {
assertEquals("Operation Not Supported", e.getMessage());
}
}
assertThat断言是JUnit4中唯一一个与其他断言相比,参数顺序相反的断言。断言具有可选的失败信息,实际值,和Matcher对象。
使用此断言来检查数组中是否包含特定值:
@Test
public void testAssertThatHasItems() {
assertThat(
Arrays.asList("Java", "Kotlin", "Scala"),
hasItems("Java", "Kotlin"));
}
JUnit5断言
JUnit5保留了JUnit4的许多断言方法,同时添加了一些利用Java 8支持的新方法。同样的,在此版本中,断言可用于所有基元类型,对象和数组。断言的参数顺序已经更改,将输出消息参数作为最后一个参数。
assertArrayEquals 断言验证预期数组和实际数组是否相等:
@Test
public void whenAssertingArraysEquality_thenEqual() {
char[] expected = { 'J', 'u', 'p', 'i', 't', 'e', 'r' };
char[] actual = "Jupiter".toCharArray();
assertArrayEquals(expected, actual, "Arrays should be equal");
}
如果数组不相等,则消息“Arrays should be equal”将显示为输出。
-### assertEquals
如果我们想断言两个浮点数相等,我们可以使用简单的assertEquals断言:
@Test
void whenAssertingEquality_thenEqual() {
float square = 2 * 2;
float rectangle = 2 * 2;
assertEquals(square, rectangle);
}
如果我们想断言实际值与预期值相差一个预定义的增量,我们可以将增量值作为第三个参数传递。:
@Test
void whenAssertingEqualityWithDelta_thenEqual() {
float square = 2 * 2;
float rectangle = 3 * 2;
float delta = 2;
assertEquals(square, rectangle, delta);
}
使用assertTrue断言,可以验证提供的条件是否为真:
@Test
void whenAssertingConditions_thenVerified() {
assertTrue(5 > 4, "5 is greater the 4");
assertTrue(null == null, "null is equal to null");
}
@Test
public void givenBooleanSupplier_whenAssertingCondition_thenVerified() {
BooleanSupplier condition = () -> 5 > 6;
assertFalse(condition, "5 is not greater then 6");
}
当我们想要断言一个对象不是空时,我们可以使用assertNotNull断言:
@Test
void whenAssertingNotNull_thenTrue() {
Object dog = new Object();
assertNotNull(dog, () -> "The dog should not be null");
}
相反,我们可以使用assertNull断言来检查实际值是否为null:
@Test
public void whenAssertingNull_thenTrue() {
Object cat = null;
assertNull(cat, () -> "The cat should be null");
}
当我们想要断言两个值是否引用相同的对象时,我们必须使用assertSame断言:
@Test
void whenAssertingSameObject_thenSuccessfull() {
String language = "Java";
Optional<String> optional = Optional.of(language);
assertSame(language, optional.get());
}
相反,我们可以使用assertNotSame。
fail断言未通过提懂得失败信息以及根本原因的测试。
@Test
public void whenFailingATest_thenFailed() {
// Test not completed
fail("FAIL - test not completed");
}
JUnit 5 中引入的新断言之一就是assertAll。此断言允许创建分组断言,其中将执行所有断言并以其报告其执行情况,详细来说,此断言接受一个标题,该标题将包含在MultipleFailureError的消息字符串中,以及一个可执行文件流。
@Test
void givenMultipleAssertion_whenAssertingAll_thenOK() {
Object obj = null;
assertAll(
"heading",
() -> assertEquals(4, 2 * 2, "4 is 2 times 2"),
() -> assertEquals("java", "JAVA".toLowerCase()),
() -> assertNull(obj, "obj is null")
);
}
assertIterableEquals断言预期和实际的Iterable是完全相等的,并且不需要两个Iterable对象具有相同的类型才能相等,只需要两个Iterable对象以相同的顺序返回相同元素。
@Test
void givenTwoLists_whenAssertingIterables_thenEquals() {
Iterable<String> al = new ArrayList<>(asList("Java", "Junit", "Test"));
Iterable<String> ll = new LinkedList<>(asList("Java", "Junit", "Test"));
assertIterableEquals(al, ll);
}
与assertArrayEquals相同,如果两个Iterable对象都为null,则认为它们是相等的。
assertLinesMatch断言预期的String列表与实际列表相匹配。
对于每行预期行和实际行,它执行一下算法:
- 检查预期行是否相等于实际行。如果是,则继续检查下一对。
- 将预期行视为正则表达式,并对实际行执行String().match()方法检查。如果是,则检查下一对。
- 检查预期的行是否为快进标记,如果是,将应用快进并重复算法步骤1。
让我们看看如何使用这个断言来断言字符串的两个列表具有匹配的行:
@Test
void whenAssertingEqualityListOfStrings_thenEqual() {
List<String> expected = asList("Java", "\\d+", "JUnit");
List<String> actual = asList("Java", "11", "JUnit");
assertLinesMatch(expected, actual);
}
assertNotEquals 断言预期值和实际值不相等:
@Test
void whenAssertingEquality_thenNotEqual() {
Integer value = 5; // result of an algorithm
assertNotEquals(0, value, "The result cannot be 0");
}
如果两者都为 null,则断言失败。
为了提高简单性和可读性,新的assertThrows断言为我们提供了一种清晰简单的方法来断言可执行文件是否引发指定的异常类型。
@Test
void whenAssertingException_thenThrown() {
Throwable exception = assertThrows(
IllegalArgumentException.class,
() -> {
throw new IllegalArgumentException("Exception message");
}
);
assertEquals("Exception message", exception.getMessage());
}
如果未引发异常,或者引发不同类型的异常,则断言将失败。
@Test
void whenAssertingTimeout_thenNotExceeded() {
assertTimeout(
ofSeconds(2),
() -> {
// code that requires less than 2 minutes to execute
Thread.sleep(1000);
}
);
}
使用assertTimeout断言时,提供的可执行文件将在调用代码的同一线程中执行,因此如果超过超时,执行不会停止。
如果我们想确保可执行文件的执行将在超过超时后中止,我们可以使用 assertTimeoutPreemptively 断言。
假设
假设仅用于满足特定条件的测试。
我们可以使用assumeTrue(),assumeFalse(),以及assumingThat()来声明假设:
@Test
void trueAssumption() {
assumeTrue(5 > 1);
assertEquals(5 + 2, 7);
}
@Test
void falseAssumption() {
assumeFalse(5 < 1);
assertEquals(5 + 2, 7);
}
@Test
void assumptionThat() {
String someString = "Just a string";
assumingThat(
someString.equals("Just a string"),
() -> assertEquals(2 + 2, 4)
);
}
如果假设测试失败,将会抛出TestAbortedException。以及该失败测试会被跳过。假设也可以使用lambda表达式。
异常测试
JUnit 5有两种测试异常的方法,都可以使用assertThrows()方法实现。
@Test
void shouldThrowException() {
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
throw new UnsupportedOperationException("Not supported");
});
assertEquals("Not supported", exception.getMessage());
}
@Test
void assertThrowsException() {
String str = null;
assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
第一个示例验证引发异常的详细信息,第二个示例验证异常的类型。
测试套件
在测试套件中聚合多个测试类,以便我们可以一起运行这些测试类。
JUnit 5 提供了两个注解:@SelectPackages和@SelectClasses,用于创建测试套件。
@Suite
@SelectPackages("com.baeldung")
@ExcludePackages("com.baeldung.suites")
public class AllUnitTest {}
@Suite
@SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})
public class AllUnitTest {}
@SelectPackages用于指定在运行测试套件时要选择的包的名称。
@SelectClasses 用于指定在运行测试套件时要选择的类。上述类将创建一个包含三个测试类的套件。这些类不必位于一个包中。
动态测试
动态测试允许我们声明和运行在运行时生成的测试用例。与静态测试相反,静态测试在编译时定义了固定数量的测试用例,动态测试允许我们在运行时动态定义测试用例。
动态测试可以通过@TestFactory注解的工厂方法生成:
@TestFactory
Stream<DynamicTest> translateDynamicTestsFromStream() {
return in.stream()
.map(word ->
DynamicTest.dynamicTest("Test translate " + word, () -> {
int id = in.indexOf(word);
assertEquals(out.get(id), translate(word));
})
);
}
此示例非常简单明了,易于理解。我们希望使用两个 ArrayList(分别命名为 in 和 out)来翻译单词。工厂方法必须返回流、集合、可迭代或迭代器。在我们的例子中,我们选择了Java 8 Stream。
请注意,@TestFactory方法不得为私有或静态。测试的数量是动态的,这取决于 ArrayList 的大小。