原创 | TDD工具集:JUnit、AssertJ和Mockito (二十)编写测试-参数化测试
重要性:★★★★☆
有时候,为了能够全面证明代码的正确性,我们需要使用多组不同的数据去测试同一个方法(例如用不同的取款金额去测试取款的结果)。如果针对每组数据分别写一个测试方法,就会非常繁琐。
通过使用
@ParameterizedTest注解取代
@Test注解,我们可以使用不同的参数值多次调用同一个测试方法,这就是参数化测试。当执行参数化测试的时候,还需要至少定义一个参数源,用来为测试方法提供参数值。
下面是简单的参数化测试例子:
@ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); }
1. 添加依赖项
要编写参数化测试,必须在项目中添加
junit-jupiter-params依赖项。
在maven项目中,需要在
pom.xml文件中的
<dependencies>节添加下面的依赖:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.6.2</version> <scope>test</scope> </dependency>
在gradle项目中,需要在
build.gradle文件中的
dependencies节添加以下内容:
testCompile 'org.junit.jupiter:junit-jupiter-params:5.6.2'
如果项目中已经定义了
junit-jupiter依赖项,就不需要添加
junit-jupiter-params依赖项了。因为前者对后者有传递性依赖。
2. 定义参数源
JUnit Jupiter提供了一些内建的参数源注解。
2.1 @ValueSource
通过指定一个由简单值字面量组成的数组提供参数源。当使用
@ValueSource时,测试方法只能有一个来自参数源的参数(依赖注入的其他参数不算)。
@ValueSource支持以下的数据类型:
- short
- byte
- int
- long
- float
- double
- char
- boolean
- java.lang.String
- java.lang.Class
例如,下面的代码示例会分别以1,2,3作为参数值调用参数化测试方法
testWithValueSource()各一次。
package yang.yu.tdd.parameterized; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; public class ParameterizedDemo { @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertThat(argument).isGreaterThan(0).isLessThan(4); } }
2.2 @NullSource
、@EmptySource
和@NullAndEmptySource
为了测试被测类方法在接收各种“坏”输入值时方法的行为,我们要给参数化测试方法提供能够提供代表null和空值的参数值。
@NullSource
:给参数化测试方法提供null作为参数值。这个注解不能用于提供基本类型的值。@EmptySource
:为参数化测试方法提供代表空(empty)
的值作为参数值。支持以下类型:java.lang.String
,java.util.List
,java.util.Set
,java.util.Map
, 基本类型数组 (例如int[]
,char[][]
, 等等), 对象数组 (例如String[]
,Integer[][]
等等,但不支持上述类型的子类型。对于字符串,会提供空字符串;对于各种集合、Map和数组,提供不包含任何元素的空集合、空Map和空数组。@NullAndEmptySource
:包含了@NullSource
和@EmptySource
的组合注解。
上述几个注解都只能应用在仅接收一个来自参数源的参数的参数化测试方法上。
下面是代码示例:
@ParameterizedTest @NullSource @EmptySource @ValueSource(strings = { " ", " ", "\t", "\n" }) void nullEmptyAndBlankStrings(String text) { assertThat(text == null || text.trim().isEmpty()).isTrue(); }
上面的参数化方法分别使用参数null, “”, " “, " “, “\t”, “\n” 调用1次,一共6次。
@NullSource提供第1个参数null,
@EmptySource提供了第2个参数””,
@ValueSource提供了其余的4个参数。参数化方法上注解出现顺序决定了参数的顺序。
如果去掉
@NullSource和
@EmptySource,换成
@NullAndEmptySource:
@ParameterizedTest @NullAndEmptySource @ValueSource(strings = { " ", " ", "\t", "\n" }) void nullEmptyAndBlankStrings2(String text) { assertThat(text == null || text.trim().isEmpty()).isTrue(); }
执行结果和上面一样。这说明
@NullAndEmptySource两次提供了参数,第一次是null,第二次是""。
2.3 @EnumSource
@EnumSource注解提供一个枚举类型的全部或部分枚举值来为参数化测试方法提供参数值。
@ParameterizedTest @EnumSource void testWithEnumSourceWithAutoDetection(ChronoUnit unit) { System.out.println(unit); } @ParameterizedTest @EnumSource(ChronoUnit.class) void testWithEnumSource(TemporalUnit unit) { System.out.println(unit); }
@EnumSource注解的值可以忽略掉。当没有给
@EnumSource注解指定值时,会使用参数化测试的第一个参数的声明类型。上面的
testWithEnumSource()方法必须给
@EnumSource注解指定值,因为
TemporalUnit不是枚举类型,而作为
TemporalUnit接口的实现,
ChronoUnit是枚举类型。
如果只想使用枚举类型中的部分枚举值,可以定义
@EnumSource注解的
names属性,包含那些要作为参数化方法的参数的枚举值:
@ParameterizedTest @EnumSource(names = { "DAYS", "HOURS" }) void testWithEnumSourceInclude(ChronoUnit unit) { assertThat(unit).isIn(ChronoUnit.DAYS, ChronoUnit.HOURS); }
还可以通过定义
@EnumSource注解的
mode属性,微调枚举值的筛选方法。它有4个取值:
- Mode.INCLUDE:默认选项。包含
names
属性中定义的枚举值。 - Mode.EXCLUDE:排除
names
属性中定义的枚举值。 - Mode.MATCH_ANY:当
names
是一组正则表达式时,返回匹配这些表达式之一的枚举值 - Mode.MATCH_ALL:当
names
是一组正则表达式时,返回匹配全部这些表达式的枚举值
@ParameterizedTest @EnumSource(mode = EnumSource.Mode.MATCH_ANY, names = "^.*DAYS$") void testWithEnumSourceRegex(ChronoUnit unit) { assertThat(unit.name()).endsWith("DAYS"); }
2.4 @MethodSource
@MethodSource注解使你可以调用测试类或外部类中的工厂方法来获得参数化测试方法的参数值。
如果工厂方法来自测试类,除非采用了
PER_CLASS生命周期,否则这个方法必须是静态的;如果工厂方法来自外部类,它必须是静态的。这些工厂方法必须没有任何参数。
每个工厂方法必须能够生成一个由参数集组成的流,每个参数集中各个参数值按顺序提供给参数化方法的各个参数。这里所说的“流”是指所有可以被JUnit转换为
Stream类型的任何类型,如
Stream,
DoubleStream,
LongStream,
IntStream,
Collection,
Iterator,
Iterable,对象数组,原始类型数组,等等。流中的元素也可以作为
Arguments类的实例、对象数组、单个值(如果参数化测试方法只接受单个参数)等提供给参数化测试方法。
下面是单个参数的代码示例:
@ParameterizedTest @MethodSource("stringProvider") void testWithExplicitLocalMethodSource(String argument) { assertThat(argument).isIn("apple", "banana"); } static Stream<String> stringProvider() { return Stream.of("apple", "banana"); }
如果你没有在
@MethodSource注解中指定工厂方法的名字,JUnit Jupiter将在测试类中寻找和参数化测试方法同名的方法作为工厂方法。下面是示例:
@ParameterizedTest @MethodSource void testWithDefaultLocalMethodSource(String argument) { assertThat(argument).isIn("apple", "banana"); } static Stream<String> testWithDefaultLocalMethodSource() { return Stream.of("apple", "banana"); }
下面的代码演示用原生流作为参数源:
@ParameterizedTest @MethodSource("range") void testWithRangeMethodSource(int argument) { assertThat(argument).isLessThan(20).isGreaterThan(9); } static IntStream range() { return IntStream.range(0, 20).skip(10); }
如果参数化测试方法声明多个参数,工厂方法必须返回以
Arguments类型的对象为元素的流(流、集合、数组等等)。下面是代码示例:
@ParameterizedTest @MethodSource("stringIntAndListProvider") void testWithMultiArgMethodSource(String str, int num, List<String> list) { assertThat(str).hasSize(5); assertThat(num).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2); assertThat(list).hasSize(2); } static Stream<Arguments> stringIntAndListProvider() { return Stream.of( Arguments.arguments("apple", 1, Arrays.asList("a", "b")), Arguments.arguments("lemon", 2, Arrays.asList("x", "y")) ); }
下面是使用外部类的静态工厂方法的例子。首先定义一个类
StringsProviders
package yang.yu.tdd.parameterized; import java.util.stream.Stream; public class StringsProviders { public static Stream<String> tinyStrings() { return Stream.of(".", "oo", "OOO"); } }
然后定义参数化测试方法,引用这个类的
tinyStrings()方法来作为参数源:
@ParameterizedTest @MethodSource("yang.yu.tdd.parameterized.StringsProviders#tinyStrings") void testWithExternalMethodSource(String tinyString) { assertThat(tinyString).isIn(".", "oo", "OOO"); }
请注意
@MethodSource注解的值是
StringsProviders类的
tinyStrings()方法的全限定名称。
2.5 @CsvSource
@CsvSource注解允许你使用CSV形式给参数化方法提供参数:
@ParameterizedTest @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 0xF1" }) void testWithCsvSource(String fruit, int rank) { assertThat(fruit).isIn("apple", "banana", "lemon, lime"); assertThat(rank).isNotEqualTo(0); }
@CsvSource注解默认以逗号作为数据项分隔符,但可以通过
delimiter属性来改用其他字符做分隔符。也可以通过设定
delimiterString属性来用指定的字符串做数据项分隔符。
@CsvSource注解使用单引号作为字符串界定符。例如上面例子中的’lemon, lime’。
''表示空字符串,除非设置了
@CsvSource注解的
emptyValue属性,那么一整个空字符串值将被作为
null值看待。
可以通过设置
@CsvSource注解的
nullValues属性,指定在CSV中出现的某些项作为null值看待。
注解 | 结果参数列表 |
---|---|
@CsvSource({ "apple, banana" }) |
"apple", "banana" |
@CsvSource({ "apple, 'lemon, lime'" }) |
"apple", "lemon, lime" |
@CsvSource({ "apple, ''" }) |
"apple", "" |
@CsvSource({ "apple, " }) |
"apple", null |
@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL") |
"apple", "banana", null |
2.6 @CsvFileSource
@CsvFileSource注解让你可以用类路径上的CSV文件来为参数化测试提供参数。
我们在类路径根目录下提供一个CSV文件
two-column.csv,内容如下:
Country, reference Sweden, 1 Poland, 2 "United States of America", 3
下面是参数化测试方法:
@ParameterizedTest @CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1, encoding = "UTF-8") void testWithCsvFileSource(String country, int reference) { assertThat(country).isIn("Sweden", "Poland", "United States of America"); assertThat(reference).isPositive(); }
注意在CSV文件中,是使用双引号而不是单引号作为字符串界定符的。
2.7 @ArgumentsSource
@ArgumentsSource注解指定一个
ArgumentsProvider的实现类,通过该类的
provideArguments方法来为参数化测试方法提供参数。这个
ArgumentsProvider必须是顶层类或静态嵌套类。
下面是代码示例:
@ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void testWithArgumentsSource(String argument) { assertThat(argument).isIn("apple", "banana"); } static class MyArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } }
3. 与其他参数共存
参数化测试方法和它的参数源提供的参数之间通常是直接一对一的关系(方法中的多个参数的出现顺序和参数源的参数出现顺序一一对应)。但是,参数化测试方法也可能从参数源聚合多个参数为一个对象传递给参数化方法的单个参数。另外参数化测试方法中还可能存在由参数解析器注入的另外的参数(例如
TestInfo和
TestReporter等)。
参数化测试方法声明形式参数必须遵循下面的规则:
- 最先声明0或多个索引的参数(由参数源提供实参的参数);
- 再声明0或多个聚合参数;
- 最后声明由参数解析器提供的参数。
4. 参数转换
4.1 拓宽转换
4.2 隐式转换
4.3 工厂方法和工厂构造函数转换
4.4 显式转换
5. 参数聚合
6. 定制显示名
7. 生命周期和互操作性
- 原创 | TDD工具集:JUnit、AssertJ和Mockito (二十三)编写测试-并行测试
- 原创 | TDD工具集:JUnit、AssertJ和Mockito (二十一)编写测试-测试模板
- 原创 | TDD工具集:JUnit、AssertJ和Mockito (二十四)编写测试-内建扩展
- 基于JUnit使用PowerMock的Mockito扩展在Maven测试项目中的配置说明
- spring Boot测试的最佳实践和测试架构的启发(JUnit4和mockito,包括MockMvc)
- Spring Test, JUnit, Mockito, Hamcrest 集成 Web 测试
- Spring Test, JUnit, Mockito, Hamcrest 集成 Web 测试
- 【Java.JUnit】Spring Test, JUnit, Mockito, Hamcrest 集成 Web 测试
- Spring Test, JUnit, Mockito, Hamcrest 集成 Web 测试
- JUnit+Mockito结合测试Spring MVC Controller
- (四)JUnit测试套件使用及参数化设置
- 如何使用Junit编写和组织测试程序
- Java单元测试工具:JUnit4(四)——JUnit测试套件使用及参数化设置
- 强大的Mockito测试框架(转)
- 基于TestNG使用PowerMock的Mockito扩展在Maven测试项目中的配置说明
- Mockito + Robolectrie + RxJava 测试MVP架构项目
- Java单元测试工具:JUnit4(四)——JUnit测试套件使用及参数化设置
- junit 参数化测试
- (原创)如何在性能测试中实现脚本参数化
- MyEclipse如何编写JUnit测试类