Android 测试框架 —— JUnit4
JUnit 4 的 官网文档:https://junit.org/junit4/
下载和安装
JUnit 4 提供了以下的方式下载和安装依赖文件:
方式 1;下载 JARs 文件,然后添加到环境中:
方式 2:通过 Maven 的方式集成:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
Note
获取 JUnit 最新版本可以访问 Maven 的官网下载。
方式 3:通过 Gradle 的方式集成:
plugins {
java
}
dependencies {
testImplementation('junit:junit:4.13')
}
断言(Assertions)
断言(Assertions)则是 JUnit 测试中的核心功能之一。通过断言,测试代码可以验证程序行为是否符合预期。JUnit
提供了一系列的断言方法,用于对代码的执行结果进行验证,从而判断测试是否通过。断言的作用是确保程序在特定的条件下运行正确。如果条件不成立,断言将抛出一个异常(通常是
AssertionError
),测试将失败,提示开发人员代码行为不符合预期。
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.matchers.JUnitMatchers.both;
import static org.junit.matchers.JUnitMatchers.containsString;
import static org.junit.matchers.JUnitMatchers.everyItem;
import static org.junit.matchers.JUnitMatchers.hasItems;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
org.junit.Assert.assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
org.junit.Assert.assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
org.junit.Assert.assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
org.junit.Assert.assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
org.junit.Assert.assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
org.junit.Assert.assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
org.junit.Assert.assertSame("should be same", aNumber, aNumber);
}
@Test
public void testAssertTrue() {
org.junit.Assert.assertTrue("failure - should be true", true);
}
}
AssertThat
除了上述的断言方法,JUnit 还提供了一个非常强大的断言方法 —— assertThat
。assertThat
属于
Hamcrest
库的一部分。Hamcrest 是一个用于创建灵活和可读性高的匹配器的库,而
assertThat
就是 Hamcrest 提供的一种断言方式。通过 assertThat
,可以构造更具表达性的测试,通常可以帮助提高代码的可读性和维护性。
assertThat
的基本语法如下:
assertThat(actual, matcher);
actual
是需要测试的值或对象。matcher
是对actual
值进行匹配的条件,通常是一个匹配器(matcher
)。
匹配器
Hamcrest 匹配器可以分成七种类型:基本匹配器、数字匹配器、集合匹配器、字符串匹配器、对象匹配器、逻辑匹配器、日期和时间匹配器、自定义匹配器。
基本匹配器
匹配器 | 描述 |
---|---|
is() |
判断实际值是否与预期值相等,通常与 assertThat 配合使用。 |
equalTo() |
匹配实际值是否等于预期值。 |
not() |
匹配实际值是否与预期值不相等。 |
sameInstance() |
判断实际对象和预期对象是否是同一个实例(即对象引用相同)。 |
samePropertyValueAs() |
判断两个对象的同一属性值是否相等。 |
数字匹配器
匹配器 | 描述 |
---|---|
greaterThan() |
匹配实际值是否大于预期值。 |
greaterThanOrEqualTo() |
匹配实际值是否大于或等于预期值。 |
lessThan() |
匹配实际值是否小于预期值。 |
lessThanOrEqualTo() |
匹配实际值是否小于或等于预期值。 |
closeTo() |
匹配实际值是否在一个给定的误差范围内接近预期值。 |
集合匹配器
匹配器 | 描述 |
---|---|
hasSize() |
匹配集合或数组的大小。 |
contains() |
匹配集合或数组中的元素是否按给定顺序出现。 |
containsInAnyOrder() |
匹配集合或数组中的元素是否包含指定元素,顺序不重要。 |
hasItem() |
匹配集合是否包含指定元素。 |
hasItems() |
匹配集合是否包含多个指定元素。 |
字符串匹配器
匹配器 | 描述 |
---|---|
containsString() |
匹配字符串是否包含指定的子字符串。 |
startsWith() |
匹配字符串是否以指定的子字符串开始。 |
endsWith() |
匹配字符串是否以指定的子字符串结束。 |
matchesPattern() |
匹配字符串是否符合指定的正则表达式。 |
对象匹配器
匹配器 | 描述 |
---|---|
instanceOf() |
匹配实际值是否是某个类的实例。 |
typeCompatibleWith() |
匹配实际值是否与指定类型兼容(即可向该类型强制转换)。 |
notNullValue() |
匹配值是否不为 null 。 |
nullValue() |
匹配值是否为 null 。 |
sameInstance() |
匹配实际值是否与预期值为同一实例。 |
refEq() |
判断对象的指定属性是否相等,适用于对象的属性比较。 |
逻辑匹配器
匹配器 | 描述 |
---|---|
both() |
用于组合两个条件,表示两个条件都必须满足。 |
either() |
用于组合两个条件,表示满足其中一个条件即可。 |
not() |
用于对某个条件的结果取反。 |
and() |
用于将多个条件连接在一起,表示所有条件都必须满足。 |
or() |
用于将多个条件连接在一起,表示至少有一个条件满足即可。 |
日期和时间匹配器
匹配器 | 描述 |
---|---|
date() |
匹配实际日期是否与预期日期相等。 |
before() |
匹配实际日期是否早于预期日期。 |
after() |
匹配实际日期是否晚于预期日期。 |
如何使用匹配器
在使用 Hamcrest 需要添加相关的库依赖:
<!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
添加完成后,就可以使用 Hamcrest 的匹配器进行断言测试:
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
public class AssertThatDemoTest {
@Test
public void test() {
assertThat(1 + 2, is(3));
assertThat(1 + 2, is(not(4)));
assertThat("Hello World!", containsString("Hello World!"));
List<String> list = Arrays.asList("Hello", "World");
assertThat(list, hasSize(2));
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
assertThat(Arrays.asList("fun", "ban", "net"), everyItem(containsString("n")));
assertThat("snowman", both(containsString("s"))
.and(containsString("n")));
assertThat(new Object(), not(sameInstance(new Object())));
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.either(equalTo(3)).or(equalTo(4))));
}
}
运行器(Test Runners)
JUnit 提供了多种 Test Runners 来执行测试,允许用户以不同的方式组织、执行和报告测试结果。Test Runners 是测试框架的核心组成部分,负责启动测试、管理测试生命周期以及报告测试结果。JUnit 主要有两类 Test Runners:默认的测试运行器和自定义测试运行器。
默认的运行器
默认情况下,JUnit 4 会使用 BlockJUnit4ClassRunner
作为测试运行器来运行 @Test
注解标记的方法。该测试运行器支持所有
JUnit 4 功能(如生命周期方法 @Before
,@After
,@BeforeClass
, @AfterClass
等)。运行器负责在执行每个测试方法之前、之后分别调用相应的生命周期方法。
RunWith
JUnit 提供了 @RunWith
注解,允许指定自定义的 Test Runner。通过 @RunWith
,可以使用不同的 Runner 来控制测试执行的方式。比如使用参数化测试器
Parameterized.class
,允许通过多组不同的数据来执行同一测试:
@RunWith(Parameterized.class)
public class CalculatorTest {
private int a;
private int b;
private int expectedResult;
public CalculatorTest(int a, int b, int expectedResult) {
this.a = a;
this.b = b;
this.expectedResult = expectedResult;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 1, 2, 3 }, { 2, 3, 5 }, { 4, 5, 9 }
});
}
@Test
public void testAddition() {
assertEquals(expectedResult, a + b);
}
}
再比如想要把多个测试类组合成一个大的测试集(Test Suite),统一执行测试用例,可以使用 @RunWith(Suite.class)
:
@RunWith(Suite.class)
@Suite.SuiteClasses({TestClass1.class, TestClass2.class})
public class AllTests {
// 这是一个测试套件,它将执行 TestClass1 和 TestClass2 中的所有测试
}
再比如 Mockito 提供的运行器用于简化使用 Mockito 进行单元测试时的设置。MockitoJUnitRunner
会自动初始化带有 @Mock
注解的字段,避免你手动调用 MockitoAnnotations.initMocks()
:
@RunWith(MockitoJUnitRunner.class)
public class MyMockitoTest {
@Mock
private MyService myService;
@Test
public void testMock() {
when(myService.doSomething()).thenReturn("Mocked!");
assertEquals("Mocked!", myService.doSomething());
}
}
除此之外,在一些特殊场景下,可以自定义一个 Test Runner 控制器来控制测试执行的流程:
public class MyCustomRunner extends Runner {
private Class<?> testClass;
public MyCustomRunner(Class<?> testClass) {
this.testClass = testClass;
}
@Override
public Description getDescription() {
return Description.createSuiteDescription(testClass);
}
@Override
public void run(RunNotifier notifier) {
// 自定义测试运行逻辑
}
}
Note
JUnit 5 引入了更强大的扩展机制,其中 @RunWith
注解被 @ExtendWith
注解替代,用于支持更多的扩展功能。JUnit 5 的测试运行器概念更加灵活,可以通过扩展接口来实现对测试执行的控制。
测试执行顺序
JUnit 设计时并没有规定测试方法的执行顺序。最初,测试方法是按反射 API 返回的顺序依次执行的。然而,依赖 JVM 的执行顺序并不是一个好选择,因为 Java 平台并没有规定特定的顺序,实际上 JDK 7 返回的是一种随机的顺序。 编写良好的测试代码不应该依赖方法执行的顺序,但有些代码可能会假设顺序。 对于某些平台来说,能预见的失败比随机失败更容易排查问题。
从 JUnit 4.11 版本开始,JUnit 默认使用一种确定性的执行顺序(即每次运行时顺序是固定的),但这个顺序仍然是不可预测的。因此
JUnit 4.13 提供了修改测试执行顺序的注解 @OrderWith
。@OrderWith
注解的参数是 Ordering.Factory
的一个实例。
JUnit 在 org.junit.tests.manipulation
包中提供了 Ordering.Factory
的实现。用户也可以创建自己的 Ordering.Factory
实例,
以实现自定义的测试方法排序。Ordering.Factory
的实现应当有一个公共构造函数,该构造函数接受一个 Ordering.Context
参数
(可以参考 Alphanumeric
源代码中的示例)。
import org.junit.Test;
import org.junit.runner.OrderWith;
import org.junit.runner.manipulation.Alphanumeric;
@OrderWith(Alphanumeric.class)
public class TestMethodOrder {
@Test
public void testA() {
System.out.println("first");
}
@Test
public void testB() {
System.out.println("second");
}
@Test
public void testC() {
System.out.println("third");
}
}
上面的代码执行顺序会按照字母的升序进行执行,Alphanumeric.class
的源码看到,Alphanumeric
继承了 Sorter
实现了 Ordering.Factory
:
public final class Alphanumeric extends Sorter implements Ordering.Factory {
public Alphanumeric() {
super(COMPARATOR);
}
public Ordering create(Context context) {
return this;
}
private static final Comparator<Description> COMPARATOR = new Comparator<Description>() {
public int compare(Description o1, Description o2) {
return o1.getDisplayName().compareTo(o2.getDisplayName());
}
};
}
从 JUnit 4.11 版本开始,可以通过简单地在测试类上使用 @FixMethodOrder
注解,并指定可用的 MethodSorters
之一,
来改变测试方法的执行顺序:
@FixMethodOrder(MethodSorters.JVM)
:按照 JVM 返回的顺序执行测试方法。这个顺序可能在不同的运行中有所不同。@FixMethodOrder(MethodSorters.NAME_ASCENDING)
:按方法名的字典顺序对测试方法进行排序。
如果没有指定 @FixMethodOrder
或 @OrderWith
注解,默认的执行顺序相当于 @FixMethodOrder(MethodSorters.DEFAULT)
。
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {
@Test
public void testA() {
System.out.println("first");
}
@Test
public void testB() {
System.out.println("second");
}
@Test
public void testC() {
System.out.println("third");
}
}
异常测试
JUnit 的异常测试(Exception Testing)是指在使用 JUnit 进行单元测试时,验证代码在遇到异常或错误条件时是否按预期抛出异常。 JUnit 支持的异常测试的有如下几种:
- 使用
assertThrows
方法。 - 使用 Try/Catch 语句。
- 在
@Test
注解上使用expected
参数。 - 使用
ExpectedException
规则。
assertThrows
assertThrows
方法是在 4.13 版本的 Assert
类中新增的。通过这个方法,可以验证某个函数调用(比如通过 lambda
表达式或方法引用指定)是否抛出了特定类型的异常。同时,assertThrows
会返回抛出的异常对象,
可以进一步对异常进行验证(比如检查异常的消息和原因是否符合预期)。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ExceptionTest {
@Test
public void testThrowException() {
// 使用 assertThrows 验证代码块是否抛出了指定异常
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Invalid argument!");
});
// 可以进一步验证异常的消息
assertEquals("Invalid argument!", thrown.getMessage());
}
@Test
public void testExceptionAndState() {
List<Object> list = new ArrayList<>();
IndexOutOfBoundsException thrown = assertThrows(
IndexOutOfBoundsException.class,
() -> list.add(1, new Object()));
// assertions on the thrown exception
assertEquals("Index: 1, Size: 0", thrown.getMessage());
// assertions on the state of a domain object after the exception has been thrown
assertTrue(list.isEmpty());
}
Try/Catch
如果使用的 4.13 以前的版本,可以结合 Try/Catch 语句进行异常测试:
@Test
public void testExceptionMessage() {
List<Object> list = new ArrayList<>();
try {
list.get(0);
fail("Expected an IndexOutOfBoundsException to be thrown");
} catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
}
}
expected
@Test
注解有一个可选的参数叫做 expected
,它接受 Throwable
的子类作为值。如果我们想验证 ArrayList
是否抛出了正确的异常,
可以这样写:
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
expected
参数应谨慎使用。上述测试方法只要方法中的任何代码抛出 IndexOutOfBoundsException
,测试就会通过。
而且,使用这种方式时,无法验证异常中的消息内容,也无法检查异常抛出后领域对象的状态。因此,推荐使用前面提到的其他方法。
ExpectedException
另一种测试异常的方法是使用 ExpectedException
规则,但这种方法在 JUnit 4.13 中已经被弃用了。
这个规则不仅可以让你指定期望的异常类型,还可以指定期望的异常消息:
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
List<Object> list = new ArrayList<Object>();
thrown.expect(IndexOutOfBoundsException.class);
thrown.expectMessage("Index: 0, Size: 0");
list.get(0); // execution will never get past this line
}
expectMessage
还允许使用 Matchers
,这为测试提供了更多的灵活性。例如:
thrown.expectMessage(CoreMatchers.containsString("Size: 0"));
这段代码会验证抛出的异常消息中是否包含 "Size: 0" 字符串。
此外,还可以使用 Matchers
来检查异常,这在异常中包含嵌入状态(例如,异常的字段或其他信息)时特别有用。例如:
thrown.expect(MyCustomException.class);
thrown.expect(Matchers.hasProperty("errorCode", Matchers.is(404)));
在这个例子中,Matchers.hasProperty
用来检查抛出的 MyCustomException
是否包含 errorCode
属性,
并且验证其值是否为 404。这种方式使你能够对异常中的具体内容进行更精确的检查。
Danger
当测试调用被测方法并且该方法抛出异常时,测试中在该方法之后的代码将不会执行(因为被测方法抛出了异常)。
这种行为可能会导致混淆,这也是 ExpectedException.none()
被弃用的原因之一。
忽略测试
如果由于某种原因,不希望一个测试失败,而是希望它被忽略,可以暂时禁用该测试。
在 JUnit 中,忽略一个测试的方法有两种方式:你可以注释掉方法,或者删除 @Test
注解;
不过,测试运行器将不会报告这样的测试。另一种方式是,在 @Test
注解前或后添加 @Ignore
注解。
测试运行器会报告被忽略的测试数量,以及已执行的测试数量和失败的测试数量。
需要注意的是,@Ignore
注解可以接受一个可选的参数(一个字符串),用来记录忽略测试的原因。
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
assertThat(1, is(1));
}
设置测试超时
对于那些“失控”或耗时过长的测试,可以自动标记为失败。实现这种行为有两种方式:
- 在
@Test
注解中添加timeout
超时。 - 使用
Timeout
规则。
@Test
添加超时
@Test(timeout=1000)
public void testWithTimeout() {
...
}
这是通过在单独的线程中运行测试方法来实现的。如果测试运行超过了分配的超时时间,测试将失败,JUnit 会中断运行测试的线程。 如果测试在执行一个可中断的操作时超时,运行测试的线程将退出(如果测试处于无限循环中,运行测试的线程将一直运行,而其他测试则会继续执行)。
Timeout 规则
Timeout
规则会将相同的超时时间应用于类中的所有测试方法,并且目前会与每个测试方法上 @Test
注解中指定的超时参数一起执行。
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
public class HasGlobalTimeout {
public static String log;
private final CountDownLatch latch = new CountDownLatch(1);
@Rule
public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds max per method tested
@Test
public void testSleepForTooLong() throws Exception {
log += "ran1";
TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
}
@Test
public void testBlockForever() throws Exception {
log += "ran2";
latch.await(); // will block
}
}
在 Timeout
规则中指定的超时适用于整个测试环境,包括任何 @Before
或 @After
方法。
如果测试方法处于无限循环中(或其他原因无法响应中断),则 @After
方法将不会被调用。
参数化测试
参数化测试(Parameterized Test)是 JUnit 提供的一种测试机制,允许在同一个测试方法中执行多次测试,每次使用不同的参数。通过这种方式,可以减少重复的测试代码,提供代码的维护性和可读性。
例如,需要给计算斐波那契数列的方法 Fibonacci
编写一个参数化测试:
public class Fibonacci {
public static int compute(int n) {
int result = 0;
if (n <= 1) {
result = n;
} else {
result = compute(n - 1) + compute(n - 2);
}
return result;
}
}
那么,使用参数化测试可以这样写:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class FibonacciTest {
// data() 方法返回一个 Collection<Object[]>。
// 这里我们使用创建一个数据集,每个 Object[] 包含两个元素:
// 第一个是 Fibonacci 数列的输入值,第二个是预期的输出值。
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{{0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}});
}
private final int fInput;
private final int fExpected;
public FibonacciTest(int fInput, int fExpected) {
this.fInput = fInput;
this.fExpected = fExpected;
System.out.printf("Input: %s, Expected: %s%n", fInput, fExpected);
}
@Test
public void test() {
assertEquals(fExpected, Fibonacci.compute(fInput));
}
}
首先,在代码中指定 JUnit 运行器(Parameterized.class
)来运行测试。这使得每次运行测试时,JUnit 会使用不同的参数调用测试方法,并执行多次测试。
然后,使用 @Parameterized.Parameters
定义提供测试数据的方法,该方法返回的数据将作为测试的输入,利用的是反射构造的方式传递参数。
最后,编写测试方法 test()
验证测试的结果。
假设(Assumptions)
在编写自动化测试时,有时某些测试只有在满足特定条件时才有意义。比如,某些功能可能依赖于操作系统的类型、机器的配置或特定的系统属性。 如果这些条件不满足,测试可能并不适用,直接失败并不能提供有用的反馈。这时,我们可以使用假设(Assumptions)来跳过测试,避免无意义的失败。
假设的主要目的是在测试执行时进行条件检查,如果某些条件不满足,测试将会跳过,而不是失败。它通过 Assume 类提供的一些方法来实现。这与 断言(Assert)方法不同,断言会在条件不满足时导致测试失败,而假设则会在条件不满足时使测试跳过。
Assume 类提供了一些静态方法,可以用来定义假设条件。这些方法包括:
Assume.assumeTrue(boolean condition)
:如果条件为 true,测试继续执行;如果条件为 false,测试将被跳过;Assume.assumeFalse(boolean condition)
:如果条件为 false,测试继续执行;如果条件为 true,测试将被跳过;Assume.assumeThat(T actual, Matcher<? super T> matcher)
:用于更复杂的条件检查,如果条件不满足,测试将被跳过;Assume.assumeNoException(Throwable exception)
:检查是否没有抛出异常,如果有异常发生,测试将被跳过。
例如,判断设备是不是 Windows 系统,若是的话才执行测试用例,那么可以这样写:
@Test
public void testForWindows() {
Assume.assumeTrue(System.getProperty("os.name").contains("Windows"));
// 只有在 Windows 上,下面的测试才会执行
// 执行测试代码
}
Warning
断言(Assert)用于验证条件是否为真,条件不满足时测试失败;而 假设(Assume)用于检查条件是否满足,条件不满足时跳过测试,不会导致失败。
规则(Rules)
规则(Rules)提供了一种机制,可以在测试执行前或后对测试进行额外的操作或配置。JUnit 规则是基于 Java 的 TestRule
接口实现的,
它允许在每个测试方法的执行前后插入自定义的行为。这些规则可以帮助我们进行日志记录、执行时间监控、资源管理、异常捕获等。
JUnit 中的规则通常实现 TestRule
接口,该接口包含一个方法 apply(Statement base, Description description)
,
通过这个方法你可以在测试方法执行前或后插入自定义逻辑。Statement
表示一个测试方法或测试类的执行语句。 通过 apply
方法,可以对测试的执行进行包装或修改。Description
提供关于当前测试的元数据,如测试方法名、类名等。
例如,自定义一个 TimerRule
规则:
public class TimerRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行测试方法
base.evaluate();
} finally {
long endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
};
}
}
使用这个规则:
public class TimerRuleTest {
@Rule
public TimerRule timerRule = new TimerRule();
@Test
public void testMethod() throws InterruptedException {
// 模拟测试方法执行
Thread.sleep(500);
}
}
JUnit 提供了一些内置的规则,可以直接使用:
规则名称 | 功能描述 |
---|---|
TemporaryFolder |
创建临时文件和目录,测试结束后会自动清理 |
ExternalResource |
用于设置在整个测试类生命周期内共享的外部资源 |
ErrorCollector |
允许收集多个错误,而不是在第一个错误发生时终止测试 |
Verifier |
用于在测试方法执行结束后,进行一些结果校验 |
TestWatcher |
用于观察每个测试方法的执行结果,并根据测试的成功与否执行相关操作 |
TestName |
提供当前正在执行的测试方法的名称 |
Timeout |
为测试方法设置执行超时,若测试超时,则自动失败 |
ExpectedException |
用于检查测试方法是否抛出了预期的异常 |
TemporaryFolder
TemporaryFolder
创建临时文件和目录,测试结束后会自动清理:
public static class HasTempFolder {
@Rule
public final TemporaryFolder folder = new TemporaryFolder();
@Test
public void testUsingTempFolder() throws IOException {
File createdFile = folder.newFile("myfile.txt");
File createdFolder = folder.newFolder("subfolder");
// ...
}
}
ExternalResource
ExternalResource
用于设置在整个测试类生命周期内共享的外部资源:
public static class UsesExternalResource {
Server myServer = new Server();
@Rule
public final ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
myServer.connect();
};
@Override
protected void after() {
myServer.disconnect();
};
};
@Test
public void testFoo() {
new Client().run(myServer);
}
}
ErrorCollector
ErrorCollector
允许收集多个错误,而不是在第一个错误发生时终止测试:
public static class UsesErrorCollectorTwice {
@Rule
public final ErrorCollector collector = new ErrorCollector();
@Test
public void example() {
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
}
}
Verifier
Verifier
用于在测试方法执行结束后,进行一些结果校验:
public class UsesVerifierTest {
private static String result;
@Rule
public final Verifier verifier = new Verifier() {
@Override
protected void verify() throws Throwable {
if ("failed".equals(result)) {
throw new AssertionError("failed");
}
}
};
@Test
public void example() {
result = "failed";
}
}
TestWatcher
TestWatcher
用于观察每个测试方法的执行结果,并根据测试的成功与否执行相关操作:
import static org.junit.Assert.fail;
import org.junit.AssumptionViolatedException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class WatchmanTest {
private static String watchedLog;
@Rule
public final TestRule watchman = new TestWatcher() {
@Override
public Statement apply(Statement base, Description description) {
return super.apply(base, description);
}
@Override
protected void succeeded(Description description) {
watchedLog += description.getDisplayName() + " " + "success!\n";
}
@Override
protected void failed(Throwable e, Description description) {
watchedLog += description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n";
}
@Override
protected void skipped(AssumptionViolatedException e, Description description) {
watchedLog += description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n";
}
@Override
protected void starting(Description description) {
super.starting(description);
}
@Override
protected void finished(Description description) {
super.finished(description);
}
};
@Test
public void fails() {
fail();
}
@Test
public void succeeds() {
}
}
Danger
TestWatchman
是在 JUnit 4.7 中引入的,它使用了已经弃用的 MethodRule
接口。详细信息可以参考:TestWatchman。
TestWatcher
从 JUnit 4.9 版本开始取代了 TestWatchman
。它实现了 TestRule
接口,而不是 MethodRule
接口。详细信息可以参考:TestWatcher。
Timeout
Timeout
为测试方法设置执行超时,若测试超时,则自动失败:
public static class HasGlobalTimeout {
public static String log;
@Rule
public final TestRule globalTimeout = Timeout.millis(20);
}
ExpectedException
ExpectedException
用于检查测试方法是否抛出了预期的异常:
public static class HasExpectedException {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void throwsNothing() {
}
@Test
public void throwsNullPointerException() {
thrown.expect(NullPointerException.class);
throw new NullPointerException();
}
@Test
public void throwsNullPointerExceptionWithMessage() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("happened?");
thrown.expectMessage(startsWith("What"));
throw new NullPointerException("What happened?");
}
}
ClassRule
@ClassRule
是一个非常重要的注解,用于定义应用于整个测试类的规则。不同于 @Rule
注解,它通常用于设置那些只需要在整个测试类开始时执行一次的规则,而不是每个测试方法执行时都执行的规则。
public class MyClassRuleTest {
@ClassRule
public static TestRule classRule = new TestRule() {
@Override
public Statement apply(Statement base, Description description) {
System.out.println("Class-level rule is being applied.");
return base;
}
};
@Test
public void test1() {
System.out.println("Test1");
}
@Test
public void test2() {
System.out.println("Test2");
}
}
Warning
@Rule
适用于需要在每个测试方法前后执行的规则。规则会在每个测试方法前后应用,适用于需要做一些测试前置或清理工作(比如设置和清理环境)的场景。
@ClassRule
适用于只需要在测试类级别执行一次的规则,通常用于全局性的资源管理(如数据库连接、文件系统、外部服务等)。
它是静态的,并且只会在测试类开始时执行一次。
RuleChain
RuleChain 规则允许对 TestRules 进行排序:
public static class UseRuleChain {
@Rule
public final TestRule chain = RuleChain
.outerRule(new LoggingRule("outer rule"))
.around(new LoggingRule("middle rule"))
.around(new LoggingRule("inner rule"));
@Test
public void example() {
assertTrue(true);
}
}
输出:
starting outer rule
starting middle rule
starting inner rule
finished inner rule
finished middle rule
finished outer rule
理论(Theories)
理论(Theories)是 JUnit 提供的一种用于执行参数化测试(Parameterized Tests)的方法,允许基于一组数据(可能是不同的输入值或测试条件)来运行不同的测试用例。
@Theory
注解使得能够创建根据不同的输入数据集自动运行的测试方法。每个理论测试方法都会根据输入数据进行多次执行,这样可以验证不同的输入数据是否会导致不同的行为。
使用 @Theory
给方法进行标注,结合 @DataPoints
或 @Theory
注解来提供数据:
import org.junit.Test;
import org.junit.experimental.theories.*;
import static org.junit.Assert.*;
public class MyTheoryTest {
// 定义一些数据点,用于提供给测试方法
@DataPoints
public static int[] numbers = {1, 2, 3, 4, 5};
// 定义理论测试方法,理论上可以使用不同的数字数据进行测试
@Theory
public void testEvenNumbers(int number) {
// 验证每个测试数字是否为偶数
assertTrue("Number is not even", number % 2 == 0);
}
// 另外一个测试方法,检查数字是否是正数
@Theory
public void testPositiveNumbers(int number) {
assertTrue("Number is not positive", number > 0);
}
}
测试夹具(Test Fixture)
测试夹具(Test Fixture)是软件测试中一个非常重要的概念。测试夹具指的是能够提供软件测试所依赖的系统、环境、状态等前置条件的方式方法。例如:
- 在测试开始前,完成特定的文本、数据输入,模拟或创建对象;
- 在测试完成后,及时清理文件、取消数据库的连接等。
JUnit 提供了一些注解,使得测试类可以在每个测试之前或之后运行夹具,或者只为所有测试方法在类级别运行一次的 Test Fixture。JUnit 有四个 Test Fixture 注解(两个类级别和两个方法级别):
@BeforeClass
和@AfterClass
类级别的注解,主要用于在整个测试类范围内执行一次初始化和清理操作, 适用于共享资源的管理。它们必须是静态方法;@Before
和@After
方法级别的注解,用于每个测试方法执行前后的初始化和清理。它们可以访问实例对象,并在每个测试方法执行时被调用。
package test;
public class TestFixturesExample {
@BeforeClass
public static void setUpClass() {
System.out.println("@BeforeClass setUpClass");
}
@AfterClass
public static void tearDownClass() throws IOException {
System.out.println("@AfterClass tearDownClass");
}
@Before
public void setUp() {
System.out.println("@Before setUp");
}
@After
public void tearDown() throws IOException {
System.out.println("@After tearDown");
}
@Test
public void test1() {
System.out.println("@Test test1()");
}
@Test
public void test2() {
System.out.println("@Test test2()");
}
}
Quote
软件测试中的 Test Fixtures 到底是什么?可以参考 Daxiong 的 这篇文章。
类别(Categories)
类别(Categories)是一种用于对测试进行分组和组织的机制。使用 JUnit Categories,可以根据测试的类型、执行时间、优先级等对测试进行分类, 从而实现有选择性地运行特定的测试。特别适用于在大型项目中对不同类型的测试进行分组(如快速测试、慢速测试、集成测试等)。
在 JUnit 中使用类别(Categories)的步骤大致可以分成三步:
Step 1:定义类别的接口
类别是通过接口来定义的,通常这些接口不包含任何方法,只作为标记接口使用:
public interface FastTests { }
public interface SlowTests { }
public interface IntegrationTests { }
Step 2:为测试方法或测试类指定类别
使用 @Category
注解来指定测试方法或测试类属于哪个类别。可以为同一个测试方法或类指定多个类别:
import org.junit.Test;
import org.junit.experimental.categories.Category;
public class MyTests {
@Test
@Category(FastTests.class)
public void testFast() {
// 快速测试的逻辑
}
@Test
@Category(SlowTests.class)
public void testSlow() {
// 慢速测试的逻辑
}
@Test
@Category(IntegrationTests.class)
public void testIntegration() {
// 集成测试的逻辑
}
@Test
@Category({FastTests.class, IntegrationTests.class})
public void testFastAndIntegration() {
// 同时属于快速测试和集成测试的测试逻辑
}
}
Step 3:使用 @RunWith(Categories.class)
来运行指定类别的测试
通过在测试类上使用 @RunWith(Categories.class)
来指定使用 Categories 运行器,并通过 @Categories.IncludeCategory
或 @Categories.ExcludeCategory
来选择运行某些类别的测试:
@RunWith(Categories.class)
@Categories.IncludeCategory(FastTests.class) // 只运行 FastTests 类别的测试
@Suite.SuiteClasses(MyTests.class)
public class RunFastTests {
}
@RunWith(Categories.class)
@Categories.ExcludeCategory(SlowTests.class) // 排除 SlowTests 类别的测试
@Suite.SuiteClasses(MyTests.class)
public class ExcludeSlowTests {
}
// 还可以组合多个类别进行选择性执行。例如:
@RunWith(Categories.class)
@Categories.IncludeCategory({FastTests.class, IntegrationTests.class})
@Suite.SuiteClasses(MyTests.class)
public class RunFastAndIntegrationTests {
// 既属于 FastTests 又属于 IntegrationTests 类别的测试。
}
Danger
JUnit 5 不再直接支持 @Category
注解,而是引入了 Tag 的概念,提供了类似的功能。在 JUnit 5 上使用使用 @Tag
注解来标记测试并根据标签过滤执行:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
public class MyTests {
@Test
@Tag("fast")
public void testFast() {
// 快速测试逻辑
}
@Test
@Tag("slow")
public void testSlow() {
// 慢速测试逻辑
}
}
然后在运行时,选择仅运行标记了特定标签的测试,例如:
mvn test -Dgroups="fast"