您的位置:首页 > 其它

原创 | TDD工具集:JUnit、AssertJ和Mockito (二十)编写测试-参数化测试

2020-06-08 16:25 204 查看


重要性:★★★★☆

有时候,为了能够全面证明代码的正确性,我们需要使用多组不同的数据去测试同一个方法(例如用不同的取款金额去测试取款的结果)。如果针对每组数据分别写一个测试方法,就会非常繁琐。

通过使用

@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. 生命周期和互操作性

点击阅读本节完整内容

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: