JUnit 4 Rule指南

2023/05/09

1. 概述

在本教程中,我们将了解JUnit 4库提供的Rule功能。

在介绍发行版提供的最重要的基本Rule之前,我们将首先介绍JUnit Rule模型。此外,我们还将了解如何编写和使用我们自己的自定义JUnit Rule

要了解有关使用JUnit进行测试的更多信息,请查看我们全面的JUnit系列

请注意,如果你使用的是JUnit 5,则Rule已被Extension模型取代

2. JUnit 4 Rule介绍

JUnit 4 Rule提供了一种灵活的机制,通过围绕测试用例执行运行一些代码来增强测试。从某种意义上说,它类似于在我们的测试类中使用@Before和@After注解

假设我们想在测试设置期间连接到外部资源(例如数据库),然后在测试完成后关闭连接。如果我们想在多个测试中使用该数据库,那么可能会在多个测试中重复相同的代码。

通过使用Rule,我们可以将所有内容隔离在一个地方,并轻松地从多个测试类中重用这些代码

3. 使用JUnit 4 Rule

那么我们如何使用Rule呢?我们可以按照以下简单步骤使用JUnit 4 Rule:

  • 在我们的测试类中添加一个公共字段,并确保该字段的类型是org.junit.rules.TestRule接口的子类型
  • 使用@Rule注解对字段进行标注

4. Maven依赖

首先,让我们添加使用JUnit所必须的JUnit 4库:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

与往常一样,我们可以从Maven Central获取最新版本。

5. JUnit中提供的Rule

当然,JUnit提供了许多有用的预定义Rule作为库的一部分。我们可以在org.junit.rules包中找到所有这些Rule

5.1 TemporaryFolder

在测试时,我们经常需要访问临时文件或文件夹。但是,管理这些文件的创建和删除可能很麻烦。使用TemporaryFolder Rule,我们可以管理在测试方法终止时应删除的文件和文件夹的创建

@Rule
public TemporaryFolder tmpFolder = new TemporaryFolder();

@Test
public void givenTempFolderRule_whenNewFile_thenFileIsCreated() throws IOException {
	File testFile = tmpFolder.newFile("test-file.txt");
    
	assertTrue("The file should have been created: ", testFile.isFile());
	assertEquals("Temp folder and test file should match: ", tmpFolder.getRoot(), testFile.getParentFile());
}

如我们所见,我们首先定义了TemporaryFolder Rule tmpFolder。接下来,我们的测试方法在临时文件夹中创建一个名为test-file.txt的文件。然后我们检查文件是否已经创建并存在于它应该存在的位置。

测试完成后,应删除临时文件夹和文件。但是,此Rule不会检查删除是否成功

在这个类中还有一些其他有趣的方法值得一提:

  • newFile()
    

    如果我们不提供任何文件名,则此方法会创建一个随机命名的新文件。

  • newFolder(String... folderNames)
    

    要创建递归深层临时文件夹,我们可以使用此方法。

  • newFolder()
    

    同样,newFolder()方法创建一个随机命名的新文件夹。

值得一提的是,从4.13版本开始,TemporaryFolder Rule允许验证已删除的资源:

@Rule 
public TemporaryFolder folder = TemporaryFolder.builder().assureDeletion().build();

如果无法删除资源,则测试失败并出现AssertionError。

最后,在JUnit 5中,我们可以使用临时目录扩展实现相同的功能。

5.2 ExpectedException

顾名思义,我们可以使用ExpectedException Rule来验证某些代码是否抛出了预期的异常:

@Rule
public final ExpectedException thrown = ExpectedException.none();

@Test
public void givenIllegalArgument_whenExceptionThrown_thenMessageAndCauseMatches() {
	thrown.expect(IllegalArgumentException.class);
	thrown.expectCause(isA(NullPointerException.class));
	thrown.expectMessage("This is illegal");
    
	throw new IllegalArgumentException("This is illegal", new NullPointerException());
}

正如我们在上面的示例中所看到的,我们首先声明了ExpectedException Rule。然后,在我们的测试中,我们断言抛出了IllegalArgumentException。

使用此Rule,我们还可以验证异常的一些其他属性,例如消息和原因

有关使用JUnit测试异常的深入指南,请阅读关于如何断言异常的快速指南。

5.3 TestName

简而言之,TestName Rule提供给定测试方法中的当前测试名称

@Rule public TestName name = new TestName();

@Test
public void givenAddition_whenPrintingTestName_thenTestNameIsDisplayed() {
	LOG.info("Executing: {}", name.getMethodName());
	assertEquals("givenAddition_whenPrintingTestName_thenTestNameIsDisplayed", name.getMethodName());
}

在这个简单的例子中,当我们运行单元测试时,我们应该在输出中看到测试名称:

INFO  [c.t.taketoday.rules.RulesUnitTest] >>> Executing: givenAddition_whenPrintingTestName_thenTestNameIsDisplayed

5.4 Timeout

此Rule提供了一种有用的替代方法,可以替代在单个@Test注解上使用timeout参数

现在,让我们看看如何使用此Rule为测试类中的所有测试方法设置全局超时:

@Rule
public Timeout globalTimeout = Timeout.seconds(10);

@Test
public void givenLongRunningTest_whenTimout_thenTestFails() throws InterruptedException {
	TimeUnit.SECONDS.sleep(20);
}

在上面的简单示例中,我们首先为所有测试方法定义10秒的全局超时。然后我们特意定义了一个需要超过10秒的测试。

当我们运行这个测试时,应该看到测试失败:

org.junit.runners.model.TestTimedOutException: test timed out after 10 seconds
...

5.5 ErrorCollector

此Rule允许在发现第一个问题后继续执行测试

让我们看看如何使用此Rule收集所有错误并在测试终止时立即报告所有错误:

@Rule 
public final ErrorCollector errorCollector = new ErrorCollector();

@Test
public void givenMultipleErrors_whenTestRuns_thenCollectorReportsErrors() {
	errorCollector.addError(new Throwable("First thing went wrong!"));
	errorCollector.addError(new Throwable("Another thing went wrong!"));
    
	errorCollector.checkThat("Hello World", not(containsString("ERROR!")));
}

在上面的示例中,我们向收集器添加了两个错误。当我们运行测试时,执行会继续,但测试最终会失败

在输出中,我们可以看到报告的两个错误:

java.lang.Throwable: First thing went wrong!
...
java.lang.Throwable: Another thing went wrong!

5.6 Verifier

Verifier Rule是一个抽象基类,当我们希望验证测试中的一些其他行为时,可以使用它。事实上,我们在上一节看到的ErrorCollector Rule扩展了这个类。

现在让我们看一个定义我们自己的验证器的简单示例:

private List messageLog = new ArrayList();

@Rule
public Verifier verifier = new Verifier() {
	@Override
	public void verify() {
		assertFalse("Message Log is not Empty!", messageLog.isEmpty());
	}
};

在这里,我们定义了一个新的Verifier并重写了verify()方法以添加一些额外的验证逻辑。在这个简单的示例中,我们只是检查示例中的messageLog是否为空。

现在,当我们运行单元测试并添加一条消息时,我们应该看到我们的验证器已被应用:

@Test
public void givenNewMessage_whenVerified_thenMessageLogNotEmpty() {
    // ...
	messageLog.add("There is a new message!");
}

5.7 DisableOnDebug

有时我们可能希望在调试时禁用Rule。例如,通常需要在调试时禁用Timeout Rule,以避免我们的测试在我们有时间正确调试之前超时和失败。

这就是DisableOnDebug Rule的作用,它允许我们在调试时标记某些要禁用的Rule:

@Rule
public DisableOnDebug disableTimeout = new DisableOnDebug(Timeout.seconds(30));

在上面的例子中我们可以看到,为了使用此Rule,我们只需将要禁用的Rule传递给构造函数即可

这个Rule的主要好处是,我们可以禁用Rule,而无需在调试期间对测试类进行任何修改。

5.8 ExternalResource

通常,在编写集成测试时,我们可能希望在测试之前设置外部资源并在测试后将其拆除。值得庆幸的是,JUnit为此提供了另一个方便的基类。

我们可以扩展抽象类ExternalResource来设置测试前的外部资源,例如文件或数据库连接。事实上,我们之前看到的TemporaryFolder Rule扩展了ExternalResource。

下面是一个扩展该类的简单示例:

@Rule
public final ExternalResource externalResource = new ExternalResource() {
    @Override
    protected void before() throws Throwable {
        // code to set up a specific external resource.
    };
    
    @Override
    protected void after() {
        // code to tear down the external resource
    };
};

在这个例子中,当我们定义一个外部资源时,我们只需要重写before()方法和after()方法来设置和拆除我们的外部资源。

6. 在类级别应用Rule

到目前为止,我们看过的所有示例都适用于单个测试用例方法。但是,有时我们可能希望在测试类级别应用Rule,我们可以通过使用@ClassRule注解来实现这一点。

这个注解的工作方式与@Rule非常相似,只是它将Rule应用在整个测试中-主要区别在于我们用于类级别Rule的字段必须是静态的

@ClassRule
public static TemporaryFolder globalFolder = new TemporaryFolder();

7. 自定义JUnit Rule

正如我们所见,JUnit 4提供了许多开箱即用的有用Rule,当然,我们可以定义自己的自定义Rule。要编写自定义Rule,我们需要实现TestRule接口

下面是一个自定义Logger和测试方法名称Rule的示例:

public class TestMethodNameLogger implements TestRule {

	private static final Logger LOG = LoggerFactory.getLogger(TestMethodNameLogger.class);

	@Override
	public Statement apply(Statement base, Description description) {
		logInfo("Before test", description);
		try {
			return new Statement() {
				@Override
				public void evaluate() throws Throwable {
					base.evaluate();
				}
			};
		} finally {
			logInfo("After test", description);
		}
	}

	private void logInfo(String msg, Description description) {
		LOG.info(msg + description.getMethodName());
	}
}

如我们所见,TestRule接口包含一个名为apply(Statement, Description)的方法,我们必须重写该方法并返回Statement的实例。Statement表示我们在JUnit运行时中的测试,当我们调用evaluate()方法时,它会执行我们的测试

在此示例中,我们再测试前后记录日志,并从Description对象中包含单个测试的方法名称。

8. 使用Rule链

在最后一节中,我们介绍如何使用RuleChain Rule对多个测试Rule进行排序

@Rule
public RuleChain chain = RuleChain.outerRule(new MessageLogger("First rule"))
    .around(new MessageLogger("Second rule"))
    .around(new MessageLogger("Third rule"));

在上面的例子中,我们创建了一个包含三个Rule的链,这些Rule简单地打印出传递给每个MessageLogger构造函数的消息。

当我们运行测试时,可以看到这些Rule是以特定顺序运行的:

Starting: First rule 
Starting: Second rule 
Starting: Third rule 
Finished: Third rule 
Finished: Second rule 
Finished: First rule 

9. 总结

在本教程中,我们详细探讨了JUnit 4中的Rule。首先,我们解释了什么是Rule以及如何使用它们;接下来,我们深入了解了JUnit开箱即用的Rule;最后,我们介绍了如何定义自己的自定义Rule以及如何将Rule链接在一起。

与往常一样,本文的完整源代码可在GitHub上找到。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章