第一次偶然发现JUnit @Rule
批注时,我对此概念有些恼火。 在测试用例中拥有一个公共领域似乎有些奇怪,因此我不愿意定期使用它。 但是一段时间后,我习惯了这一点,事实证明,规则可以通过多种方式简化编写测试的过程。 这篇文章简要介绍了该概念,并简要列举了一些规则的优点。
什么是JUnit规则?
让我们从一个现成的JUnit规则开始。 TemporaryFolder
是一个测试帮助程序,可用于为临时内容1创建位于文件系统目录下的文件和文件夹。 TemporaryFolder
的有趣之处在于,它保证在测试方法完成时删除其文件和文件夹2 。 为了按预期方式工作,必须将临时文件夹实例分配给@Rule
注释字段,该字段必须是公共的(不是静态的),并且是TestRule
的子类型:
public class MyTest {@Rulepublic TemporaryFolder temporaryFolder = new TemporaryFolder();@Testpublic void testRun() throws IOException {assertTrue( temporaryFolder.newFolder().exists() );}
}
它是如何工作的?
规则提供了一种拦截测试方法调用的可能性,就像AOP框架一样。 与AspectJ中的周围建议相比,您可以在实际测试执行之前和/或之后做一些有用的事情3 。 尽管这听起来很复杂,但是却很容易实现。
规则定义的API部分必须实现TestRule。 此接口称为apply
的唯一方法返回Statement
。 Statement
s表示(简单地说)在JUnit运行时中的测试,而Statement#evaluate()
执行它们。 现在,基本思想是提供Statement
包装扩展,该包装可以通过覆盖Statement#evaluate()
来进行实际贡献:
public class MyRule implements TestRule {@Overridepublic Statement apply( Statement base, Description description ) {return new MyStatement( base );}
}public class MyStatement extends Statement {private final Statement base;public MyStatement( Statement base ) {this.base = base;}@Overridepublic void evaluate() throws Throwable {System.out.println( 'before' );try {base.evaluate();} finally {System.out.println( 'after' );}}
}
MyStatement
作为包装器实现,在MyRule#apply(Statement,Destination)
使用该包装器包装作为参数给出的原始语句。 很容易看出,包装程序覆盖了Statement#evaluate()
在实际测试4之前和之后做一些事情。
下一个代码片段显示如何与上面的TemporaryFolder
完全一样地使用MyRule
:
public class MyTest {@Rulepublic MyRule myRule = new MyRule();@Testpublic void testRun() {System.out.println( 'during' );}
}
启动测试用例将导致以下控制台输出,这证明我们的示例规则可以按预期工作。 测试执行被我们的规则拦截和修改,以在测试的“期间”前后打印“之前”和“之后”:
before
during
after
现在已经了解了基础知识,下面让我们看一下您可以使用规则执行的更有用的事情。
测试治具
从相应的维基百科部分引用的“测试装置”是运行测试并期望获得特定结果所必须具备的所有条件。 通常,通过处理单元测试框架的setUp()
和tearDown()
事件来创建固定装置。
使用JUnit,这通常看起来像这样:
public class MyTest {private MyFixture myFixture;@Testpublic void testRun1() {myFixture.configure1();// do some testing here}@Testpublic void testRun2() {myFixture.configure2();// do some testing here}@Beforepublic void setUp() {myFixture = new MyFixture();}@Afterpublic void tearDown() {myFixture.dispose();}
}
考虑您在许多测试中以上面显示的方式使用特定的夹具。 在那种情况下,最好摆脱setUp()
和tearDown()
方法。 鉴于以上各节,我们现在知道可以通过更改MyFixture
来实现TestRule
来完成。 适当的Statement
实现必须确保它调用MyFixture#dispose()
并且看起来可能像这样:
public class MyFixtureStatement extends Statement {private final Statement base;private final MyFixture fixture;public MyFixtureStatement( Statement base, MyFixture fixture ) {this.base = base;this.fixture = fixture;}@Overridepublic void evaluate() throws Throwable {try {base.evaluate();} finally {fixture.dispose();}}
}
有了这个,上面的测试可以重写为:
public class MyTest {@Rulepublic MyFixture myFixture = new MyFixture();@Testpublic void testRun1() {myFixture.configure1();// do some testing here}@Testpublic void testRun2() {myFixture.configure2();// do some testing here}
}
在很多情况下,我开始欣赏使用规则编写测试的更为紧凑的形式,但是可以肯定的是,这也是一个品味问题以及您认为更适合阅读的内容5 。
带有方法注释的夹具配置
到目前为止,我已默默地忽略了TestRule#apply(Statement,Description)
的Description
参数。 通常, Description
描述了将要运行或已经运行的测试。 但它也允许访问有关底层java方法的一些反射信息。 除其他外,有可能读取这种方法附带的注释。 这使我们能够将规则与方法注释结合起来,以方便配置TestRule
。
考虑以下注释类型:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Configuration {String value();
}
与MyFixture#apply(Statement,Destination)
中的以下代码段结合使用,该代码读取注释为特定测试方法的配置值…
Configuration annotation= description.getAnnotation( Configuration.class );
String value = annotation.value();
// do something useful with value
…上面演示MyFixture
规则用法的MyFixture
可以重写为:
public class MyTest {@Rulepublic MyFixture myFixture = new MyFixture();@Test@Configuration( value = 'configuration1' )public void testRun1() {// do some testing here}@Test@Configuration( value = 'configuration2' )public void testRun2() {// do some testing here}
}
当然,由于注释仅允许Enum
, Class
es或String
文字作为参数,因此后一种方法存在局限性。 但是在某些用例中,这已经足够了。 restfuse库提供了一个很好的示例,该示例将规则与方法注释结合使用。 如果您对现实世界的示例感兴趣,则应查看Destination
规则6的库实现。
最后,剩下的唯一要说的是,我很想听听您关于可以用来简化日常测试工作的JUnit规则的其他有用示例的信息:
- 通常由
System.getProperty( 'java.io.tmpdir' );
返回的目录System.getProperty( 'java.io.tmpdir' );
↩ - 在查看
TemporaryFolder
的实现时,我必须注意,它不会检查文件删除是否成功。 这可能是打开的文件句柄的情况下,一个薄弱点↩ - 值得的是,您甚至可以用其他方法代替完整的测试方法↩
- 包装语句的委托放入
try...finally
块中,以确保执行测试后的功能,即使测试失败。 在这种情况下,一个AssertionError
会被抛出,并且不是在finally块语句都将跳过↩ - 你可能注意到
TemporaryFolder
之初例子也不外乎夹具的使用情况↩ - 请注意,restfuse的
Destination
类实现了MethodRule
而不是TestRule
。 这篇文章基于最新的JUnit版本,其中MethodRule
被标记为@Deprecated
。TestRule
代替MethodRule
。 但是,鉴于此职位的知识,仍然应该很容易理解实现↩
参考:来自JCG合作伙伴 Frank Appel的JUnit规则 ,位于Code Affine博客上。
翻译自: https://www.javacodegeeks.com/2012/11/junit-rules.html