Junit 4 Rules 介绍
一个 Junit Rule(规则) 就是一个实现了org.junit.rules.TestRule
接口的类,这些类的作用类似于@Before
@After
注解,通过在类的执行前后运行指定的代码逻辑来增强测试,此外,Junit Rule
还能做一些@Before
@After
注解实现不了的功能,如动态获取测试类、测试方法的信息
假设我们的测试用例需要在运行的时候连接外部资源(如数据库),在测试结束的时候释放连接,如果我们想在多个测试类中使用该数据库,那么最终将在每个测试类中重复该代码
通过使用 Rule,我们可以将重复的逻辑隔离在一个地方,并方便的在多个测试类中重用该代码
Rule 使用
引入 maven 依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
Rule 使用遵循以下几个步骤
- 在测试类中添加一个
public
的字段,该字段类型需是org.junit.rules.TestRule
接口的实现类 - 使用
@Rule
注解标记该字段
junit4 内置规则介绍
Junit 默认提供了很多 Rule 供开发者使用,下面介绍几种常见的 Rule
TemporaryFolder Rule
测试的时候,我们经常需要创建一个临时文件/文件夹,但是在测试类中管理这些文件/文件夹有时候会很麻烦,这时使用 TemporaryFolderRule
就能很好的帮我们管理测试过程中的文件的创建与删除:
public class TemporaryFolderRuleTest {
@Rule
public TemporaryFolder tmpFolder = new TemporaryFolder();
@Test
public void test() 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
规则,然后通过tmpFolder.newFile("test-file.txt")
方法,创建了一个临时文件test-file.txt
,当测试执行完的时候,TemporaryFolder
会自动删除掉临时文件跟文件夹(但不校验删除是否成功)
从 junit 4.13 开始,
TemporaryFolder
支持配置校验是否删除成功,删除失败则抛出AssertionError
异常
@Rule public TemporaryFolder folder = TemporaryFolder.builder().assureDeletion().build();
TemporaryFolder
除了newFile(String fileName)
方法外,还有几个常用的方法:
newFile()
创建一个随机名称的文件newFolder(String... folderNames)
根据多级目录文件夹,如newFolder("a","b")
创建文件夹a/b
newFolder()
创建一个随机名称的文件夹
ExpectedException Rule
ExpectedException
很好理解,通过名称就能大致猜出它是用来校验异常的,ExpectedException
可以用来校验代码即将发生的异常:
public class ExpectedExceptionRuleTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void test() {
thrown.expect(IllegalArgumentException.class);
thrown.expectCause(isA(NullPointerException.class));
thrown.expectMessage("This is illegal");
throw new IllegalArgumentException("This is illegal", new NullPointerException());
}
}
在测试方法中,我们断言了接下来的代码会抛出IllegalArgumentException
,且case
是NullPointerException
,message
是This is illegal
TestName Rule
TestName
可以很方便的让我们获取当前执行的测试方法的名称:
public class TestNameRuleTest {
@Rule
public TestName name = new TestName();
@Test
public void test() {
System.out.println("method name = " + name.getMethodName());
assertEquals("test", name.getMethodName());
}
}
执行输出:
method name = test
Timeout Rule
对于添加了 Timeout
Rule 的测试类,当测试类中的测试方法执行超过 Timeout
Rule 配置的时间时,测试方法执行就会被标记为失败:
public class TimeoutRuleTest {
@Rule
public Timeout globalTimeout = Timeout.seconds(5);
@Test
public void timeout() throws InterruptedException {
TimeUnit.SECONDS.sleep(10);
}
@Test
public void onTime() throws InterruptedException {
TimeUnit.SECONDS.sleep(2);
}
}
执行上面测试用例,onTime
方法执行通过,timeout()
方法则抛出TestTimedOutException
:
org.junit.runners.model.TestTimedOutException: test timed out after 5 seconds
ErrorCollector Rule
顾名思义,ErrorCollector Rule
就是用来收集 Error
的,正常情况下,当测试方法抛异常的时候,方法执行会中断。使用 ErrorCollector
可以把异常收集起来,让测试用例继续执行,待执行用例跑完之后再将异常信息报告出来
public class ErrorCollectorRuleTest {
@Rule
public final ErrorCollector errorCollector = new ErrorCollector();
@Test
public void test() {
errorCollector.addError(new Throwable("First thing went wrong!"));
errorCollector.addError(new Throwable("Second thing went wrong!"));
errorCollector.checkThat("Hello World", not(containsString("ERROR!")));
System.out.println("run finished");
}
}
执行 test
方法程序输出如下:
run finished
java.lang.Throwable: First thing went wrong!
...
java.lang.Throwable: Second thing went wrong!
...
Process finished with exit code 255
从执行结果可以看出,程序先打印 run finished
再输出错误信息。说明 Errorcollector
暂时先讲错误信息给收集起来,待测试用例执行完再将报错信息抛出,整个测试用例的执行结果为 fail
Verifier Rule
Verifier
是一个抽象类,当我们需要在测试用例中验证一些额外的行为的时候,可以使用这个类并重写该类的 verify
方法。事实上,有些 Rule
就是通过继承 Verifier
类实现的
下面定义一个 Verifier
public class VerifierRuleTest {
private List messageLog = new ArrayList<>();
@Rule
public Verifier verifier = new Verifier() {
@Override
protected void verify() throws Throwable {
assertFalse("Message Log is not Empty!", messageLog.isEmpty());
}
};
}
该 Verifier
做的事情很简单,就是在每个测试用例执行完的时候额外验证一下 messageLog
是否为空。接下来写两个用例验证一下
@Test
public void successTest() {
messageLog.add("I love java");
}
@Test
public void failTest() {
System.out.println("I don't like java");
}
执行两个用例,第一个用例成功,但是第二个用例报错如下:
java.lang.AssertionError: Message Log is not Empty!
DisableOnDebug Rule
有时候我们希望在 debug 的时候 disable 掉某些 Rule,比如我们的测试代码中定义了一个Timeout Rule
,但 debug 的时候往往会超出 Timeout Rule
设定的超时时间,这时候就可以使用 DisableOnDebug Rule
将 Timeout Rule
给 disable 掉,这样当我们通过 debug 模式执行测试用例时,Timeout Rule
就不会生效
public class DisableOnDebugRuleTest {
@Rule
public DisableOnDebug disableTimeout = new DisableOnDebug(Timeout.seconds(5));
@Test
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(10);
}
}
当使用 debug 模式执行测试用例的时候,用例能通过。直接 run 测试用例的时候会失败,并报TestTimedOutException
ExternalResource Rule
当做集成测试的时候,有可能需要在执行前准备一些数据(可以是一个文件/数据库连接),并在执行完将准备的数据进行删除。这时候就可以用 ExternalResource Rule
实现,前面所讲的 TemporaryFolder Rule
正式通过集成 ExternalResource
实现的,详情可以参考 TemporaryFolder
源码
@ClassRule 使用
上面介绍的都是使用 @Rule
注解,@Rule
作用于测试方法级别的。如果想让 Rule 作用于 class 级别,可以使用 @ClassRule
注解(类似于@Before、@After跟@BeforeClass、@AfterClass的区别)
@ClassRule
public TemporaryFolder tmpFolder = new TemporaryFolder();
自定义 Junit Rule
除了使用 junit
提供的默认 Rule
外,还可以自定义我们自己的 Rule
, 自定义 Rule
需要继承 TestRule
接口,并实现其 apply
方法
public class CustomerRuleTest {
@Rule
public TestMethodNameLogger testMethodNameLogger = new TestMethodNameLogger();
@Test
public void test() {
System.out.println("run case");
}
static class TestMethodNameLogger implements TestRule {
@Override
public Statement apply(Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println("before test,class name is " + description.getClassName());
statement.evaluate();
System.out.println("after test,class name is " + description.getClassName());
}
};
}
}
}
上面的例子,我们自定义了一个 TestMethodNameLogger Rule
该 Rule
的作用是在执行测试方法执行前后打印出 log
apply
方法中的 Statement
参数表示测试方法执行过程中的 runtime
,当调用 statement.evaluate()
方法时,则执行测试方法中的代码
Description
参数可以获取到当前正在执行的测试用例的一些信息(类名,方法名等)
Rule 链
当有多个 Rule
的时候,可以通过 RuleChain
来控制多个 Rule
的执行顺序,类似于过滤器链(FilterChain)
public class RuleChainsTest {
@Rule
public RuleChain ruleChain = RuleChain.outerRule(new SimpleMsgLogger("First Rule"))
.around(new SimpleMsgLogger("Second Rule"))
.around(new SimpleMsgLogger("Third Rule"));
@Test
public void test() {
System.out.println("run case");
}
static class SimpleMsgLogger implements TestRule {
private String log;
public SimpleMsgLogger(String log) {
this.log = log;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println("Starting:" + log);
base.evaluate();
System.out.println("Finishing:" + log);
}
};
}
}
}
执行结果如下:
Starting:First Rule
Starting:Second Rule
Starting:Third Rule
run case
Finishing:Third Rule
Finishing:Second Rule
Finishing:First Rule
Rule 使用场景示例
写过 spring
项目的都知道,当我们想要在测试类中使用 spring
容器,需要在测试类上添加 @RunWith(SpringRunner.class)
注解,但此时如果你又想用 junit
的参数化测试的话,就会有冲突产生,因为 junit
参数化测试需要在测试类上添加 @RunWith(Parameterized.class)
注解
我们知道,一个测试类不能同时添加两个 @RunWith
注解,对于这种情况,就可以使用 junit Rule
解决
可以使用 @SpringClassRule
@SpringMethodRule
这两个注解来替代 @RunWith(SpringRunner.class)
启动 spring
容器
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WiremockApplication.class)
public class RuleTest {
@ClassRule
public static SpringClassRule springClassRule = new SpringClassRule();
@Rule
public SpringMethodRule springMethodRule = new SpringMethodRule();
@Autowired
public SampleController sampleController;
@Test
public void test() {
sampleController.hello();
}
}
示例代码地址: https://github.com/HuangXiaoyu3331/test-study/tree/main/src/test/java/com/hxy/junit/rule
参考: https://www.baeldung.com/junit-4-rules