1. Overview
1.概述
JUnit 5, the next generation of JUnit, facilitates writing developer tests with shiny new features.
JUnit 5是新一代的JUnit,它以闪亮的新功能促进了开发人员测试的编写。
One such feature is parameterized tests. This feature enables us to execute a single test method multiple times with different parameters.
其中一个特点是p参数化测试。这个功能使我们能够用不同的参数多次执行一个测试方法。
In this tutorial, we’re going to explore parameterized tests in depth, so let’s get started.
在本教程中,我们将深入探讨参数化测试,所以让我们开始吧。
2. Dependencies
2.依赖性
In order to use JUnit 5 parameterized tests, we need to import the junit-jupiter-params artifact from JUnit Platform. That means, when using Maven, we’ll add the following to our pom.xml:
为了使用JUnit 5参数化测试,我们需要从JUnit平台导入junit-jupiter-params神器。这意味着,使用Maven时,我们要在pom.xml中添加以下内容。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
Also, when using Gradle, we’ll specify it a little differently:
另外,在使用Gradle的时候,我们会以一种不同的方式来指定它。
testCompile("org.junit.jupiter:junit-jupiter-params:5.8.1")
3. First Impression
3.第一印象
Let’s say we have an existing utility function, and we’d like to be confident about its behavior:
假设我们有一个现有的效用函数,而且我们想对它的行为有信心。
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
Parameterized tests are like other tests except that we add the @ParameterizedTest annotation:
参数化测试和其他测试一样,只是我们添加了@ParameterizedTest注解。
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
JUnit 5 test runner executes this above test — and consequently, the isOdd method — six times. And each time, it assigns a different value from the @ValueSource array to the number method parameter.
JUnit 5测试运行器执行了上述测试–以及随之而来的isOdd方法–六次。每一次,它都从@ValueSource数组中分配一个不同的值给number方法参数。
So, this example shows us two things we need for a parameterized test:
因此,这个例子向我们展示了参数化测试需要的两件事。
- a source of arguments, in this case, an int array
- a way to access them, in this case, the number parameter
There is still another aspect not evident with this example, so we’ll keep looking.
还有一个方面在这个例子中并不明显,所以我们将继续寻找。
4. Argument Sources
4.论据来源
As we should know by now, a parameterized test executes the same test multiple times with different arguments.
我们现在应该知道,一个参数化测试用不同的参数多次执行同一个测试。
And we can hopefully do more than just numbers, so let’s explore.
而我们希望能做的不仅仅是数字,所以让我们来探讨。
4.1. Simple Values
4.1.简单的价值观
With the @ValueSource annotation, we can pass an array of literal values to the test method.
通过@ValueSource 注解,我们可以向测试方法传递一个字面值的数组。
Suppose we’re going to test our simple isBlank method:
假设我们要测试我们简单的isBlank方法。
public class Strings {
public static boolean isBlank(String input) {
return input == null || input.trim().isEmpty();
}
}
We expect from this method to return true for null for blank strings. So, we can write a parameterized test to assert this behavior:
我们期望从这个方法返回true的空白字符串的null。因此,我们可以写一个参数化的测试来断言这一行为。
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
As we can see, JUnit will run this test two times and each time assigns one argument from the array to the method parameter.
我们可以看到,JUnit将运行这个测试两次,每次都从数组中分配一个参数给方法参数。
One of the limitations of value sources is that they only support these types:
价值来源的局限性之一是,它们只支持这些类型。
- short (with the shorts attribute)
- byte (bytes attribute)
- int (ints attribute)
- long (longs attribute)
- float (floats attribute)
- double (doubles attribute)
- char (chars attribute)
- java.lang.String (strings attribute)
- java.lang.Class (classes attribute)
Also, we can only pass one argument to the test method each time.
此外,我们每次只能向测试方法传递一个参数。。
Before going any further, note that we didn’t pass null as an argument. That’s another limitation — we can’t pass null through a @ValueSource, even for String and Class.
在进一步讨论之前,请注意我们没有把null作为一个参数传递。这是另一个限制–我们不能通过@ValueSource传递null,即使是String 和Class。
4.2. Null and Empty Values
4.2.空值和空值
As of JUnit 5.4, we can pass a single null value to a parameterized test method using @NullSource:
从JUnit 5.4开始,我们可以使用@NullSource向参数化测试方法传递一个单一的null 值。
@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
assertTrue(Strings.isBlank(input));
}
Since primitive data types can’t accept null values, we can’t use the @NullSource for primitive arguments.
由于原始数据类型不能接受null值,我们不能对原始参数使用@NullSource。
Quite similarly, we can pass empty values using the @EmptySource annotation:
相当类似地,我们可以使用@EmptySource 注解来传递空值。
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@EmptySource passes a single empty argument to the annotated method.
@EmptySource 向被注解的方法传递一个空参数。
For String arguments, the passed value would be as simple as an empty String. Moreover, this parameter source can provide empty values for Collection types and arrays.
对于String参数,传递的值将是一个简单的空String。此外,这个参数源可以为Collection类型和数组提供空值。。
In order to pass both null and empty values, we can use the composed @NullAndEmptySource annotation:
为了同时传递null和空值,我们可以使用组成@NullAndEmptySource注释。
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
As with the @EmptySource, the composed annotation works for Strings, Collections, and arrays.
与@EmptySource一样,组成注解适用于Strings、Collections和array。
To pass a few more empty string variations to the parameterized test, we can combine @ValueSource, @NullSource, and @EmptySource together:
为了向参数化测试传递更多的空字符串变化,我们可以将@ValueSource, @NullSource,和@EmptySource 结合起来。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
4.3. Enum
4.3. 枚举
In order to run a test with different values from an enumeration, we can use the @EnumSource annotation.
为了用来自枚举的不同值运行测试,我们可以使用@EnumSource注解。
For example, we can assert that all month numbers are between 1 and 12:
例如,我们可以断言所有月份的数字都在1和12之间。
@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
Or, we can filter out a few months by using the names attribute.
或者,我们可以通过使用names 属性来过滤掉几个月。
We could also assert the fact that April, September, June and November are 30 days long:
我们还可以断言,4月、9月、6月和11月都是30天的事实。
@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
By default, the names will only keep the matched enum values.
默认情况下,names将只保留匹配的枚举值。
We can turn this around by setting the mode attribute to EXCLUDE:
我们可以通过将mode属性设置为EXCLUDE来扭转这一局面。
@ParameterizedTest
@EnumSource(
value = Month.class,
names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(31, month.length(isALeapYear));
}
In addition to literal strings, we can pass a regular expression to the names attribute:
除了字面字符串之外,我们还可以向names 属性传递一个正则表达式。
@ParameterizedTest
@EnumSource(value = Month.class, names = ".+BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {
EnumSet<Month> months =
EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);
assertTrue(months.contains(month));
}
Quite similar to @ValueSource, @EnumSource is only applicable when we’re going to pass just one argument per test execution.
与@ValueSource相当类似,@EnumSource只适用于我们要在每次测试执行中只传递一个参数的情况。
4.4. CSV Literals
4.4 CSV字样
Suppose we’re going to make sure that the toUpperCase() method from String generates the expected uppercase value. @ValueSource won’t be enough.
假设我们要确保 toUpperCase()方法从String产生预期的大写字母值。@ValueSource将是不够的。
To write a parameterized test for such scenarios, we have to
为了给这种情况写一个参数化的测试,我们必须
- Pass an input value and an expected value to the test method
- Compute the actual result with those input values
- Assert the actual value with the expected value
So, we need argument sources capable of passing multiple arguments.
因此,我们需要能够传递多个参数的参数源。
The @CsvSource is one of those sources:
@CsvSource就是这些来源之一。
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
The @CsvSource accepts an array of comma-separated values, and each array entry corresponds to a line in a CSV file.
@CsvSource接受一个由逗号分隔的值组成的数组,每个数组条目对应于CSV文件中的一行。
This source takes one array entry each time, splits it by comma and passes each array to the annotated test method as separate parameters.
这个源码每次取一个数组条目,用逗号分割,并将每个数组作为单独的参数传递给注释的测试方法。
By default, the comma is the column separator, but we can customize it using the delimiter attribute:
默认情况下,逗号是列的分隔符,但我们可以使用delimiter 属性对其进行自定义。
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
String actualValue = input.toLowerCase();
assertEquals(expected, actualValue);
}
Now it’s a colon-separated value, so still a CSV.
现在它是一个用冒号分隔的值,所以还是CSV。
4.5. CSV Files
4.5.CSV文件
Instead of passing the CSV values inside the code, we can refer to an actual CSV file.
我们可以参考一个实际的CSV文件,而不是在代码中传递CSV值。
For example, we could use a CSV file like this:
例如,我们可以使用这样一个CSV文件。
input,expected
test,TEST
tEst,TEST
Java,JAVA
We can load the CSV file and ignore the header column with @CsvFileSource:
我们可以加载CSV文件,用@CsvFileSource来忽略标题列。
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
The resources attribute represents the CSV file resources on the classpath to read. And, we can pass multiple files to it.
resources属性表示要读取classpath上的CSV文件资源。而且,我们可以向它传递多个文件。
The numLinesToSkip attribute represents the number of lines to skip when reading the CSV files. By default, @CsvFileSource does not skip any lines, but this feature is usually useful for skipping the header lines like we did here.
numLinesToSkip属性表示读取CSV文件时要跳过的行数。默认情况下,@CsvFileSource 不会跳过任何行,但这个功能通常对跳过标题行很有用,就像我们在这里做的那样。
Just like the simple @CsvSource, the delimiter is customizable with the delimiter attribute.
就像简单的@CsvSource一样,定界符可以通过delimiter属性来定制。
In addition to the column separator, we have these capabilities:
除了分栏器之外,我们还有这些功能。
- The line separator can be customized using the lineSeparator attribute — a newline is the default value.
- The file encoding is customizable using the encoding attribute — UTF-8 is the default value.
4.6. Method
4.6. 方法
The argument sources we’ve covered so far are somewhat simple and share one limitation. It’s hard or impossible to pass complex objects using them.
到目前为止,我们所涉及的参数源都比较简单,而且有一个共同的局限性。使用它们来传递复杂的对象是困难的,甚至是不可能的。
One approach to providing more complex arguments is to use a method as an argument source.
提供更复杂的参数的一种方法是使用一个方法作为参数源。。
Let’s test the isBlank method with a @MethodSource:
让我们用@MethodSource来测试isBlank方法。
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
The name we supply to @MethodSource needs to match an existing method.
我们提供给@MethodSource的名字需要与一个现有的方法相匹配。
So, let’s next write provideStringsForIsBlank, a static method that returns a Stream of Arguments:
所以,我们接下来写provideStringsForIsBlank,一个静态方法,返回一个Arguments的Stream。
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
Here we’re literally returning a stream of arguments, but it’s not a strict requirement. For example, we can return any other collection-like interfaces like List.
在这里,我们实际上是在返回一个参数流,但这并不是一个严格的要求。例如,我们可以返回任何其他类似集合的接口,如List.。
If we’re going to provide just one argument per test invocation, then it’s not necessary to use the Arguments abstraction:
如果我们打算在每个测试调用中只提供一个参数,那么就没有必要使用Arguments抽象。
@ParameterizedTest
@MethodSource // hmm, no method name ...
void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument(String input) {
assertTrue(Strings.isBlank(input));
}
private static Stream<String> isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument() {
return Stream.of(null, "", " ");
}
When we don’t provide a name for the @MethodSource, JUnit will search for a source method with the same name as the test method.
当我们没有为@MethodSource提供名称时,JUnit将搜索一个与测试方法同名的源方法。
Sometimes, it’s useful to share arguments between different test classes. In these cases, we can refer to a source method outside of the current class by its fully qualified name:
有时,在不同的测试类之间共享参数是很有用的。在这种情况下,我们可以用完全合格的名称来引用当前类之外的源方法。
class StringsUnitTest {
@ParameterizedTest
@MethodSource("com.baeldung.parameterized.StringParams#blankStrings")
void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {
assertTrue(Strings.isBlank(input));
}
}
public class StringParams {
static Stream<String> blankStrings() {
return Stream.of(null, "", " ");
}
}
Using the FQN#methodName format, we can refer to an external static method.
使用FQN#methodName格式,我们可以引用一个外部静态方法。
4.7. Custom Argument Provider
4.7.自定义参数提供者
Another advanced approach to pass test arguments is to use a custom implementation of an interface called ArgumentsProvider:
另一种传递测试参数的高级方法是使用一个名为ArgumentsProvider的接口的自定义实现。
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
Then we can annotate our test with the @ArgumentsSource annotation to use this custom provider:
然后,我们可以用@ArgumentsSource 注解来使用这个自定义的提供者,来注释我们的测试。
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
Let’s make the custom provider a more pleasant API to use with a custom annotation.
让我们用自定义注解使自定义提供者成为一个更令人愉快的API来使用。
4.8. Custom Annotation
4.8.自定义注释
Suppose we want to load the test arguments from a static variable:
假设我们想从一个静态变量加载测试参数。
static Stream<Arguments> arguments = Stream.of(
Arguments.of(null, true), // null strings should be considered blank
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(
String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
Actually, JUnit 5 does not provide this. However, we can roll our own solution.
实际上,JUnit 5并没有提供这个功能。但是,我们可以推出自己的解决方案。
First, we can create an annotation:
首先,我们可以创建一个注释。
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {
/**
* The name of the static variable
*/
String value();
}
Then we need to somehow consume the annotation details and provide test arguments. JUnit 5 provides two abstractions to achieve those:
然后,我们需要以某种方式获取注解细节,并提供测试参数。JUnit 5提供了两个抽象来实现这些。
- AnnotationConsumer to consume the annotation details
- ArgumentsProvider to provide test arguments
So, we next need to make the VariableArgumentsProvider class read from the specified static variable and return its value as test arguments:
所以,我们接下来需要让VariableArgumentsProvider类从指定的静态变量中读取并返回其值作为测试参数。
class VariableArgumentsProvider
implements ArgumentsProvider, AnnotationConsumer<VariableSource> {
private String variableName;
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return context.getTestClass()
.map(this::getField)
.map(this::getValue)
.orElseThrow(() ->
new IllegalArgumentException("Failed to load test arguments"));
}
@Override
public void accept(VariableSource variableSource) {
variableName = variableSource.value();
}
private Field getField(Class<?> clazz) {
try {
return clazz.getDeclaredField(variableName);
} catch (Exception e) {
return null;
}
}
@SuppressWarnings("unchecked")
private Stream<Arguments> getValue(Field field) {
Object value = null;
try {
value = field.get(null);
} catch (Exception ignored) {}
return value == null ? null : (Stream<Arguments>) value;
}
}
And it works like a charm.
而且,它的效果非常好。
5. Argument Conversion
5.论据转换
5.1. Implicit Conversion
5.1.隐式转换
Let’s re-write one of those @EnumTests with a @CsvSource:
让我们用@CsvSource重写其中的一个@EnumTests。
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
This seems like it shouldn’t work, but it somehow does.
这似乎不应该起作用,但不知为何却起作用了。
JUnit 5 converts the String arguments to the specified enum type. To support use cases like this, JUnit Jupiter provides a number of built-in implicit type converters.
JUnit 5将String 参数转换为指定的枚举类型。为了支持像这样的用例,JUnit Jupiter提供了一些内置的隐式类型转换器。
The conversion process depends on the declared type of each method parameter. The implicit conversion can convert the String instances to types such as the following:
转换过程取决于每个方法参数的声明类型。隐式转换可以将String实例转换为诸如以下类型。
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
- File and Path
- URL and URI
- Enum subclasses
5.2. Explicit Conversion
5.2.明确的转换
We sometimes need to provide a custom and explicit converter for arguments.
我们有时需要为参数提供一个自定义的、明确的转换器。
Suppose we want to convert strings with the yyyy/mm/dd format to LocalDate instances.
假设我们想把具有yyyy/mm/dd格式的字符串转换为LocalDate实例。
First, we need to implement the ArgumentConverter interface:
首先,我们需要实现ArgumentConverter接口。
class SlashyDateConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
if (!(source instanceof String)) {
throw new IllegalArgumentException(
"The argument should be a string: " + source);
}
try {
String[] parts = ((String) source).split("/");
int year = Integer.parseInt(parts[0]);
int month = Integer.parseInt(parts[1]);
int day = Integer.parseInt(parts[2]);
return LocalDate.of(year, month, day);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert", e);
}
}
}
Then we should refer to the converter via the @ConvertWith annotation:
然后我们应该通过@ConvertWith注解来引用转换器。
@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
assertEquals(expected, date.getYear());
}
6. Argument Accessor
6.参数访问器
By default, each argument provided to a parameterized test corresponds to a single method parameter. Consequently, when passing a handful of arguments via an argument source, the test method signature gets very large and messy.
默认情况下,提供给参数化测试的每个参数都对应着一个方法参数。因此,当通过参数源传递少量的参数时,测试方法的签名会变得非常大和混乱。
One approach to address this issue is to encapsulate all passed arguments into an instance of ArgumentsAccessor and retrieve arguments by index and type.
解决这个问题的一个方法是将所有传递的参数封装到ArgumentsAccessor的实例中,并通过索引和类型检索参数。
Let’s consider our Person class:
让我们考虑一下我们的Person类。
class Person {
String firstName;
String middleName;
String lastName;
// constructor
public String fullName() {
if (middleName == null || middleName.trim().isEmpty()) {
return String.format("%s %s", firstName, lastName);
}
return String.format("%s %s %s", firstName, middleName, lastName);
}
}
To test the fullName() method, we’ll pass four arguments: firstName, middleName, lastName, and the expected fullName. We can use the ArgumentsAccessor to retrieve the test arguments instead of declaring them as method parameters:
为了测试fullName()方法,我们将传递四个参数。firstName,middleName,lastName和expected fullName。我们可以使用ArgumentsAccessor来检索测试参数,而不是将它们声明为方法参数。
@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {
String firstName = argumentsAccessor.getString(0);
String middleName = (String) argumentsAccessor.get(1);
String lastName = argumentsAccessor.get(2, String.class);
String expectedFullName = argumentsAccessor.getString(3);
Person person = new Person(firstName, middleName, lastName);
assertEquals(expectedFullName, person.fullName());
}
Here, we’re encapsulating all passed arguments into an ArgumentsAccessor instance and then, in the test method body, retrieving each passed argument with its index. In addition to just being an accessor, type conversion is supported through get* methods:
在这里,我们将所有传递的参数封装到一个ArgumentsAccessor实例中,然后,在测试方法主体中,用索引检索每个传递的参数。除了只是一个访问器外,类型转换还通过get*方法得到支持。
- getString(index) retrieves an element at a specific index and converts it to String — the same is true for primitive types.
- get(index) simply retrieves an element at a specific index as an Object.
- get(index, type) retrieves an element at a specific index and converts it to the given type.
7. Argument Aggregator
7.参数聚合器
Using the ArgumentsAccessor abstraction directly may make the test code less readable or reusable. In order to address these issues, we can write a custom and reusable aggregator.
直接使用ArgumentsAccessor抽象可能会使测试代码的可读性或可重用性降低。为了解决这些问题,我们可以写一个自定义的、可重用的聚合器。
To do that, we implement the ArgumentsAggregator interface:
为了做到这一点,我们实现了ArgumentsAggregator接口。
class PersonAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
return new Person(
accessor.getString(1), accessor.getString(2), accessor.getString(3));
}
}
And then we reference it via the @AggregateWith annotation:
然后我们通过@AggregateWith注释来引用它。
@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(
String expectedFullName,
@AggregateWith(PersonAggregator.class) Person person) {
assertEquals(expectedFullName, person.fullName());
}
The PersonAggregator takes the last three arguments and instantiates a Person class out of them.
PersonAggregator接收最后三个参数,并从它们中实例化出一个Person类。
8. Customizing Display Names
8.自定义显示名称
By default, the display name for a parameterized test contains an invocation index along with a String representation of all passed arguments:
默认情况下,参数化测试的显示名称包含一个调用索引,以及所有通过的参数的String代表。
├─ someMonths_Are30DaysLongCsv(Month)
│ │ ├─ [1] APRIL
│ │ ├─ [2] JUNE
│ │ ├─ [3] SEPTEMBER
│ │ └─ [4] NOVEMBER
However, we can customize this display via the name attribute of the @ParameterizedTest annotation:
然而,我们可以通过@ParameterizedTest注释的name属性来定制这种显示。
@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
April is 30 days long surely is a more readable display name:
四月有30天长肯定是一个更易读的显示名称。
├─ someMonths_Are30DaysLong(Month)
│ │ ├─ 1 APRIL is 30 days long
│ │ ├─ 2 JUNE is 30 days long
│ │ ├─ 3 SEPTEMBER is 30 days long
│ │ └─ 4 NOVEMBER is 30 days long
The following placeholders are available when customizing the display name:
在定制显示名称时,可使用以下占位符。
- {index} will be replaced with the invocation index. Simply put, the invocation index for the first execution is 1, for the second is 2, and so on.
- {arguments} is a placeholder for the complete, comma-separated list of arguments.
- {0}, {1}, ... are placeholders for individual arguments.
9. Conclusion
9.结论
In this article, we explored the nuts and bolts of parameterized tests in JUnit 5.
在这篇文章中,我们探讨了JUnit 5中参数化测试的核心和细节。
We learned that parameterized tests are different from normal tests in two aspects: they’re annotated with the @ParameterizedTest, and they need a source for their declared arguments.
我们了解到,参数化测试在两个方面与普通测试不同:它们被注释为@ParameterizedTest,并且它们需要一个声明参数的来源。
Also, by now, we should know that JUnit provides some facilities to convert the arguments to custom target types or to customize the test names.
另外,到现在为止,我们应该知道JUnit提供了一些设施,可以将参数转换为自定义目标类型或自定义测试名称。
As usual, the sample codes are available on our GitHub project, so make sure to check it out.
像往常一样,样本代码可以在我们的GitHub项目中找到,所以请务必查看。