当前位置:网站首页>Junit5中的参数化测试(Parameterized Tests)指南
Junit5中的参数化测试(Parameterized Tests)指南
2022-06-24 15:33:00 【独家雨天】
作为新一代的测试框架,Junit5中有很多大家喜欢的测试方案,个人认为最突出的就是能够进行参数化的测试(Parameterized Tests)。
简介
通常,会遇到这样的情况,同一个测试案例,改变的只是测试时候输入的参数不同。按照之前的做法,可能会是通过每个输入参数都写一个测试,或者将测试参数封装到集合中循环遍历执行测试。在新版的Junit5中,已经提供了一种更加优雅的方式来进行。
该特性允许我们:该特性可以让我们运行单个测试多次,且使得每次运行仅仅是参数不同而已。
安装依赖
为了使用 JUnit 5 的参数化测试(parameterized tests)。需要在Junit Platform的基础上,导入而外的 junit-jupiter-params 架包。
maven:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
Gradle:
testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")
简单的案例
比如,需要测试一个函数是判断输入值否为基数。
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
通过Parameterized Test,则可以写成如下的形式:
@ParameterizedTest
@ValueSource(ints = {
1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
JUnit5 将会执行上面的测试6次,每次都会分配来之*@ValueSource*中不同的int参数.
如何定义不同参数的来源
上面简单的展示了如何通过不同的参数来运行同一个测试。但是很多时候并不仅仅是Int类型。
简单值 Simple Value
@ValueSource 可以往测试方法中传递一个数据或者迭代器。可支持的简单参数如下:
- short (with the shorts attribute)
- byte (with the bytes attribute)
- int (with the ints attribute)
- long (with the longs attribute)
- float (with the floats attribute)
- double (with the doubles attribute)
- char (with the chars attribute)
- java.lang.String (with the strings attribute)
- java.lang.Class (with the classes attribute)
值得注意的是,@ValueSource不允许传入Null值和Empty值。从JUnit 5.4开始,我们可以使用@NullSource、@EmptySource 和 @NullAndEmptySource 注解可以分别将单个null值、单个Empty和 Null和Empty 传递给参数化测试方法。
枚举类 Enum
为了运行将一个枚举类的所有的值传入到测试中,可以使用 @EnumSource注解。比如使用枚举类Month
@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
或者通过names可以过滤掉一些某些枚举类
@ParameterizedTest
@EnumSource(value = Month.class, names = {
"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
通常的情况下,会被认为names是对匹配的这些名字的枚举类进行操作,但是通过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));
}
另外,也可以通过在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));
}
CSV(最常用)
很多时候,需要同时传入参数和预期结果,来验证测试逻辑。比如, 需要去测试toUpperCase()方法(能够将预期的String字符串转换成预期的大写字母字符串)。
@ParameterizedTest
@CsvSource({
"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
其中 @CsvSource 注解接受一个以逗号分隔的数组,并且每个数组项都对应于CSV文件中的一行。该注解包含了一个 delimiter 属性,可以用来定义分割符(默认是逗号)。
CSV Files
同前面的CSV一样,只是把参数写到具体的CSV文件存储起来。通过@CsvFileSource注解说明文件路径。
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
通常情况下,@CsvFileSource注解回去解析每一行,但有些时候,第一行可能会是列名,所以在上面的方法中加上了numLinesToSkip 属性来跳过第一行。
方法 Method
通过@MethodSource注解可以传递一些复杂的迭代对象到测试中。
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
其中@MethodSource 注解需要匹配现有存在方法,通常会在同测试类中查询该方法,如果不在同测试类文件下,则需要加上方法名的全限定名。比如下面例子
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, "", " ");
}
}
自定义参数提供器 Custom Argument Provider
通过实现ArgumentsProvider接口可以使用一些更加高级的方式去传递参数。比如
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
然后在实际的测试类中引用上面的自定义参数提供器。
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
如何自定义注解
前面的都是Junit 参数化解析框架提供的注解,也可以自定义一些参数注解,以一种更加优美的方式来实现参数化得测试。比如,想实现一个从静态变量里面加载测试参数的注解。类似于如下的代码。
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));
}
JUnit5 提供了如下的两个基类来帮助我们实现。一个用来实现注解细节,一个用来提供测试参数。
- AnnotationConsumer 基类提供注解细节
- ArgumentsProvider 基类提供测试的参数
实际的实现如下:
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;
}
}
参数的类型转换
隐式转换
假设通过@CsvSource 注解来重写了前面@EmumTests 测试。在@CSVSource中通过传入字符串,而不是枚举类。
@ParameterizedTest
@CsvSource({
"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
按理来说是应该失败的,但是实际运行你会发现,它能够正常的运行。
因为Junit5默认会对字符串进行隐式的转换。String默认可以转换成如下的几种类型
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
- File and Path
- URL and URI
- Enum subclasses
显式转换
有些时候,需要提供一种自定义的显式参数类型转换器。例如,想将 yyyy/mm/dd 格式的字符串数据转换成LocalDate实例。
第一步是实现 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);
}
}
}
然后通过 @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());
}
参数存取器
通常情况下,一个测试参数,会对应一个形参参数。但是当通过一个参数源传递多个参数的时候,则有些时候就会显得很大和混乱。这时候,可以通过一个参数存取器 ArgumentsAccessor来聚合这些参数,然后在使用的时候,根据索引来获得。比如,想测试下面的Person类中的 fullName方法
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);
}
}
如果想测试fullName方法,则需要传入 firstName, middleName, lastName, 和 the 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());
}
(讲道理,没有看出太大的优势)好处就是将所有的参数都聚合存储在一起,并且通过下面定义的几个方法来解析。
- getString(index) 直接通过索引解析了具体的值,成字符串。(返回类型就是String)
- get(index) 简单通过索引元素解析成 Object对象
- get(index, type) 将指定的索引元素解析成指定的类型对象 type
参数聚合器
使用前面的参数存取器 ArgumentsAccessor可能会使得代码缺少可读性和复用性。可以提通过自定义一个aggregator来实现。
首先就是实现 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));
}
}
然后通过指定 @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());
}
上面的 PersonAggregator用例中,聚合了最后的3个参数,并且实例化出了一个Person对象。
自定义显式名称 Customizing Display Names
默认情况下,测试运行之后显式的测试名如下
├─ someMonths_Are30DaysLongCsv(Month)
│ │ ├─ [1] APRIL
│ │ ├─ [2] JUNE
│ │ ├─ [3] SEPTEMBER
│ │ └─ [4] NOVEMBER
但是我们可以通过 @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’ 可能更加的表意。
在自定义显示名下,可以使用下面的几个占位符。
- {index} 表示调用索引,从1开始,然后2,3等
- {arguments} 表示完整的参数列表,以逗号分隔。
- **{0}, {1}, …****.*表示单个参数名称。
总结
Junit5 会越来越流行,上面的相关源代码请参考 tutorials/testing-modules/junit5-annotations at master · eugenp/tutorials。
参考文档
边栏推荐
- Redis consistency hash and hash slot
- Crmeb multi merchant system applet authorization problem solving paste
- Is industrial securities reliable? Is it safe to open a securities account?
- Closed loop management of time synchronization service -- time monitoring
- Kubernetes practical tips: using ksniff to capture packets
- 10 hands-free idea plug-ins. These codes do not need to be written (the second bullet)
- How to optimize performance
- Use list
- A full set of tutorials for interviewers from Android manufacturers teach you: prepare for the interview and get the offer smoothly!
- 在同花顺开户证券安全吗,需要什么准备
猜你喜欢

Method after charging the idea plug-in material theme UI

Linux记录-4.22 MySQL5.37安装(补充)

CVPR 2022 - Interpretation of selected papers of meituan technical team

As a developer, what is the most influential book for you?

还在担心漏测吗?快来使用jacoco统计下代码覆盖率

Do you really know the difference between H5 and applet?

Stm32f1 and stm32cubeide programming examples -ws2812b full color LED driver (based on spi+dma)

Two way combination of business and technology to build a bank data security management system

Bert whitening vector dimension reduction and its application

From pair to unordered_ Map, theory +leetcode topic practice
随机推荐
安装wireshark时npcap怎么都安装不成功,建议先用winpcap
Monitoring and warning | is the website attacked?
Wide measuring range of jishili electrometer
Openinstall joins hands with the book chain to help channel data analysis and create the era of Book Networking
【我的OpenGL学习进阶之旅】OpenGL的坐标系的学习笔记
Esp32 series -- comparison of esp32 series
兴业证券靠谱吗?开证券账户安全吗?
[sdx62] wcn685x IPA registration failure analysis and solution
This website teaches you to imitate more than 100 well-known websites!
Analysis of dompurify
update+catroot+c000021a+critical service failed+drivers+intelide+viaide+000000f
07. Tencent cloud IOT device side learning - Data Template
In 2021, big companies often ask IOS interview questions -- runloop
10 hands-free idea plug-ins. These codes do not need to be written (the second bullet)
Industry cases of successful digital transformation
Design of vga/lcd display controller system based on FPGA (Part 2)
时间同步业务的闭环管理——时间监测
中国十大证券app排名 炒股开户安全吗
SF express: please sign for MySQL soul ten
为什么企业实施WMS仓储管理系统很容易失败