跳转至

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 还提供了一个非常强大的断言方法 —— assertThatassertThat 属于 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"