关于JUnit测试要点的多篇教程的第四章介绍了该工具可交换测试运行器体系结构的目的,并介绍了一些可用的实现。 正在进行的示例通过编写参数化测试的不同可能性扩大了主题。
由于我已经发布了JUnit Rules的介绍,因此我决定跳过关于该主题的已宣布部分。 相反,我对后者进行了较小的更新。
测试跑步者架构
不要害怕为了伟大而放弃美好。
约翰·洛克菲勒
在先前的文章中,我们学习了将某些xUnit测试模式[MES]与JUnit一起使用。 工具运行时的默认行为很好地支持了这些概念。 但是有时需要针对特定的测试类型或目标更改或补充后者。
考虑例如集成测试 ,该测试通常需要在特定环境中运行。 或想象一组包含子系统规范的测试用例,应该为常见的测试执行而组成。
为此,JUnit支持使用各种类型的测试处理器。 因此,它在运行时将测试类实例化,测试执行和结果报告委托给此类处理器,这些处理器必须是org.junit.Runner
子类型。
测试用例可以使用@RunWith
注释指定其预期的运行器类型。 如果未指定任何类型,那么运行时将选择BlockJUnit4ClassRunner
作为默认值。 负责每个测试运行一个新的测试实例,并调用诸如隐式设置或拆卸处理程序之类的生命周期方法(另请参见有关测试结构的章节)。
@RunWith( FooRunner.class )
public class BarTest {
该代码段显示了如何将虚构的FooRunner
指定为也是虚构的BarTest
测试处理器。
通常,无需编写自定义测试运行程序。 但是,如果需要的话, Michael Scharhag最近就对JUnit的运行器体系结构进行了很好的解释。
看起来特殊测试运行程序的用法很简单,所以让我们看一下其中的几个:
套房和类别
Suite
可能是最著名的处理器之一。 它允许以分层或主题结构的方式运行测试和/或其他套件的集合。 注意,指定类本身通常没有主体实现。 它带有一系列测试类的注释,这些类通过运行套件来执行:
@RunWith(Suite.class)
@SuiteClasses( { NumberRangeCounterTest.class,// list of test cases and other suites
} )
public class AllUnitTests {}
但是,套件的结构功能受到一定限制。 因此,JUnit 4.8引入了鲜为人知的Categories
概念。 这样就可以定义自定义类别类型,例如单元测试,集成测试和验收测试。 要将测试用例或方法分配给这些类别之一,必须提供Category
注释:
// definition of the available categories
public interface Unit {}
public interface Integration {}
public interface Acceptance {}// category assignment of a test case
@Category(Unit.class)
public class NumberRangeCounterTest {[...]
}// suite definition that runs tests
// of the category 'Unit' only
@RunWith(Categories.class)
@IncludeCategory(Unit.class)
@SuiteClasses( { NumberRangeCounterTest.class,// list of test cases and other suites
} )
public class AllUnitTests {}
带Categories
注释类定义了只运行与指定类别匹配的类列表测试的套件。 通过包含和/或排除注释来完成规范。 请注意,类别可以在Maven或Gradle构建中使用,而无需定义特定的套件类(请参见JUnit文档的类别部分)。
有关类别的更多信息: John Ferguson Smart撰写了有关使用JUnit类别对测试进行分组的详细说明。
由于通常认为套件类列表和类别注释的维护有些繁琐,因此您可能更喜欢通过测试后缀名称àFooUnitTest而不是FooTest进行分类。 这允许在运行时在类型范围上过滤类别。
但是JUnit本身不支持此过滤,因此您可能需要一个特殊的运行器来动态收集可用的匹配测试。 Johannes Link的ClasspathSuite
是提供适当实现的库。 如果您碰巧在OSGi环境中进行集成测试,则Rüdiger的BundleTestSuite
会对捆绑BundleTestSuite
执行类似的操作。
在对如何将测试运行程序用于测试捆绑的第一印象之后,让我们继续本教程的示例,并进行一些更令人兴奋的事情。
参数化测试
本教程中使用的示例都是关于编写一个简单的数字范围计数器,该计数器从给定值开始传递一定数量的连续整数。 另外,计数器取决于存储类型以保留其当前状态。 有关更多信息,请参阅前面的章节。
现在,假定应将由构造函数参数初始化的NumberRangeCounter
作为API提供。 因此,我们可以认为实例创建检查给定参数的有效性是合理的。
我们可以指定适当的极端情况,并通过每个测试通过IllegalArgumentException
予以确认。 使用带有Java 8 Lambdas方法的Clean JUnit Throwable-Tests方法,这种验证storage参数一定不能为null的测试可能看起来像这样:
@Testpublic void testConstructorWithNullAsStorage() {Throwable actual = thrown( () -> new NumberRangeCounter( null, 0, 0 ) );assertTrue( actual instanceof IllegalArgumentException );assertEquals( NumberRangeCounter.ERR_PARAM_STORAGE_MISSING,actual.getMessage() );}
请注意,我坚持使用JUnit内置功能进行验证。 我将在另一篇文章中介绍特定匹配器库( Hamcrest , AssertJ )的优缺点。
为了使该职位保持范围,我还跳过了有关NPE是否比IAE更好的讨论。
如果我们必须涵盖很多这种极端情况,则上述方法可能会导致很多非常相似的测试。 JUnit提供了Parameterized
实现,以减少这种冗余。 这个想法是为通用测试结构提供各种数据记录。
为此,使用带有@Parameters
注释的公共静态方法来创建数据记录作为对象数组的集合。 此外,测试用例需要一个带有参数的公共构造函数,该参数与记录提供的数据类型匹配。
参数化处理器针对由参数方法提供的每个记录运行给定测试。 这意味着对于每种测试和记录组合,都会创建一个新的测试类实例。 构造函数参数将存储为字段,并且可以通过测试进行访问以进行设置,练习和验证:
@RunWith( Parameterized.class )
public class NumberRangeCounterTest {private final String message;private final CounterStorage storage;private final int lowerBound;private final int range;@Parameterspublic static Collection<Object[]> data() {CounterStorage dummy = mock( CounterStorage.class );return Arrays.asList( new Object[][] { { NumberRangeCounter.ERR_PARAM_STORAGE_MISSING, null, 0, 0 }, { NumberRangeCounter.ERR_LOWER_BOUND_NEGATIVE, dummy, -1, 0 },[...] // further data goes here... } );}public NumberRangeCounterTest(String message, CounterStorage storage, int lowerBound, int range ){this.message = message;this.storage = storage;this.lowerBound = lowerBound;this.range = range;}@Testpublic void testConstructorParamValidation() {Throwable actual = thrown( () -> new NumberRangeCounter( storage, lowerBound, range ) );assertTrue( actual instanceof IllegalArgumentException );assertEquals( message, actual.getMessage() );}[...]
}
尽管该示例确实减少了测试冗余,但至少在可读性方面值得商bat。 最后,这通常取决于测试的数量和特定测试数据的结构。 但绝对不幸的是, 不使用任何记录值的测试也将执行多次。
因此,参数化测试通常保存在单独的测试用例中,通常感觉更像是一种解决方法,而不是适当的解决方案。 因此,一个聪明的人想到了提供一种可以避免上述问题的测试处理器的想法。
JUnitParams
JUnitParams库提供JUnitParamsRunner
和@Parameter
类型。 param批注指定给定测试的数据记录。 注意使用相同简单名称的JUnit批注的区别。 后者标记了提供数据记录的方法!
可以使用JUnitParams重写上面的测试方案,如以下代码片段所示:
@RunWith( JUnitParamsRunner.class )
public class NumberRangeCounterTest {public static Object data() {CounterStorage dummy = mock( CounterStorage.class );return $( $( ERR_PARAM_STORAGE_MISSING, null, 0, 0 ),$( ERR_LOWER_BOUND_NEGATIVE, dummy, -1, 0 ) ); }@Test@Parameters( method = "data" )public void testConstructorParamValidation(String message, CounterStorage storage, int lowerBound, int range ) {Throwable actual = thrown( () -> new NumberRangeCounter( storage, lowerBound, range ) );assertTrue( actual instanceof IllegalArgumentException );assertEquals( message, actual.getMessage() );}[...]
}
虽然这当然更紧凑并且乍一看看上去更干净,但仍有一些构造需要进一步说明。 $(...)
方法是在JUnitParamsRunner
(静态导入)中定义的,是创建对象数组的快捷方式。 一旦习惯了,数据定义就会变得更具可读性。
$
快捷方式用于方法data
以创建嵌套的对象数组作为返回值。 尽管运行程序期望在运行时嵌套数据数组,但它能够将简单的对象类型作为返回值来处理。
测试本身具有一个附加的@Parameters
批注。 批注的方法声明是指用于为测试提供声明的参数的数据提供程序 。 方法名称在运行时通过反射解析。 这是解决方案的缺点,因为它在编译时不安全。
但是在其他用例场景中,您可以指定数据提供程序类或隐式值,因此不会受到这种折衷的影响。 有关更多信息,请参见例如该库的快速入门指南 。
另一个巨大的优点是,现在只有那些测试针对使用@Parameters
批注的数据记录运行。 标准测试仅执行一次。 反过来,这意味着可以将参数化测试保留在设备的默认测试用例中。
包起来
以上各节概述了JUnit可交换测试运行器体系结构的意义和目的。 它介绍了套件和类别以显示基本用法,并举例说明了测试运行程序如何简化编写与数据记录相关的测试的任务。
有关其他测试运行程序的列表,junit.org上的“ 测试运行程序”和“ 自定义运行程序 ”页面可能是一个不错的起点。 并且,如果您想知道标题图片的Theories
主题是什么,可以看看Florian Waibels在JUnit上 发表的文章– Practice和@Theory之间的区别 。
下次在Nutshell中使用JUnit时,我最后将介绍可用于验证测试结果的各种断言。
参考文献
[MES] xUnit测试模式,Gerard Meszaros,2007年
翻译自: https://www.javacodegeeks.com/2014/09/junit-in-a-nutshell-test-runners.html