用csv文件进行Junit5的参数化测试
2018-05-23 17:06:51 +08 字数:2321 标签: Java Test一些函数式的接口,给定输入、期待特定输出,没有太多副作用,特别适合参数化测试(Parameterized Test)。 JUnit5提供了多种参数化测试的形式,本文着重介绍CsvSource和CsvFileSource。
本文以leetcode上的第一个问题——Two Sum——为例,解释如何用JUnit5做参数化测试。
问题描述与输入输出 ¶
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1].
给定输入nums = [2, 7, 11, 15]与target = 9,期待输出为[0, 1],因为:
$$ nums[0] + nums[1] = 2 + 7 = 9 $$
被测试Java代码的形式如下:
public class Solution {
public int[] twoSum(int[] nums, int target) {
// Ignore the implementation code
}
}
一般测试手段 ¶
一般的测试手段,就是一个测试写一个方法(Method)。
class TwoSumTest {
void twoSum0() {
int[] nums = new int[]{2, 7, 11, 15};
int target = 9;
int[] expect = new int[]{0, 1};
TwoSum solution = new TwoSum();
int[] result = solution.twoSum(nums, target);
assertArrayEquals(expect, result);
}
void twoSum1() {
int[] nums = new int[]{3, 2, 3};
int target = 6;
int[] expect = new int[]{0, 2};
TwoSum solution = new TwoSum();
int[] result = solution.twoSum(nums, target);
assertArrayEquals(expect, result);
}
}
显然,其中有大量的重复代码。 在新增一个测试case时,需要大量的复制粘贴,而且很不方便。
还有一种写法,是把所有测试全都写到一个方法中。 这相当于在一个测试case中,做了多组测试。 其问题在于,在某一组输入条件测试失败时,无法快速定位到是哪一组。 这种写法,还不如分开写。
CsvSource ¶
CsvSource是JUnit5支持的一个注解(Annotation)。
它和ParameterizedTest一样,都属于额外的junit-jupiter-params这个Group。
使用前,需要在build.gradle中添加依赖。
ext.junitJupiterVersion = '5.1.0'
dependencies {
...
testCompile "org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}"
}
使用示例如下:
class TwoSumTest {
@ParameterizedTest
@CsvSource({
"'[1, 1]', 2, '[0, 1]'",
"'[3, 2, 3]', 6, '[0, 2]'",
"'[2, 7, 11, 15]', 9, '[0, 1]'"
})
void twoSum(
@ConvertWith(String2int.class) int[] nums,
int target,
@ConvertWith(String2int.class) int[] expect
) {
TwoSum solution = new TwoSum();
assertArrayEquals(expect, solution.twoSum(nums, target));
}
}
通过@CsvSource传入的每一项,就相当于一个csv文件的一行,是一个String。
每一行中,可以有若干列,这里是三列,代表测试函数的三个输入参数的一组值。
为了让值中包含分隔符,,需要用单引号''包含。
如果是基本数据类型,如int、String之类,JUnit5会尝试直接转换。
而如果是不常见的数据类型,则需要使用@ConvertWith。
上一段代码中的@ConvertWith(String2int.class) int[] nums,,就是在用一个自定义类String2int.class,把String类型转换为int[]。
class String2int implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
try {
String str = (String) source;
str = str.trim().substring(1, str.length() - 1).trim();
if (str.isEmpty()) return new int[]{};
if (!str.contains(",")) return new int[]{Integer.parseInt(str)};
return Arrays.stream(str.split(","))
.map(String::trim)
.mapToInt(Integer::parseInt).toArray();
} catch (ClassCastException e) {
throw new ArgumentConversionException("The source is not a String", e);
} catch (NumberFormatException e) {
throw new ArgumentConversionException("Some content in source is not int", e);
}
}
}
String2int.class需要实现ArgumentConverter的convert方法,进行String到int[]的转换。
CsvFileSource ¶
既然可以用csv结构的方式来输入测试参数,那么可以不可以用csv文件来作为数据源? 当然可以。 JUnit5通过CsvFileSource,支持直接输入一个csv文件。
@ParameterizedTest
@CsvFileSource(resources = "/two_sum.csv", numLinesToSkip = 1)
void twoSum(
@ConvertWith(String2int.class) int[] nums,
int target,
@ConvertWith(String2int.class) int[] expect
) {
assertArrayEquals(expect, solution.twoSum(nums, target));
}
其中,resources = "/two_sum.csv"是指定csv文件,这个文件在测试代码的resources目录下。
按照默认的Gradle目录结构(如下),two_sum.csv需要放在src/test/resources/目录下。
src
├── main
│ ├── java
│ └── resources
└── test
├── java
└── resources
numLinesToSkip = 1是略过文件第1行,因为第一行通常是csv列名。
以下为csv文件内容示例,与CsvSource那边略有不同:
nums,target,expect
"[1, 1]",2,"[0, 1]"
"[3, 2, 3]",6,"[0, 2]"
"[2, 7, 11, 15]",9,"[0, 1]"
这里选用[2, 7, 11, 15]这种形式的字符串来作为输入源,是借鉴了Python的表达方式。
实际上,CsvSource的形式非常易于不同语言之间共享测试case。
无论是CsvSource还是CsvFileSource,如果某一个单元格的内容为空,比如三个参数都为空的一行,,,在参数转换时会得到null。
如果为了避免null而使用空字符串"",则需要在CsvSource中用"'','',''"、在CsvFileSource中用"","",""。
可以参考《CsvSource with empty strings instead of null · Issue #1014 · junit-team/junit5》。
测试结果 ¶
无论参数化测试怎么写,都可以在执行gradle test时,得到以下形式的测试结果。
$ gradle test
> Task :junitPlatformTest
╷
└─ JUnit Jupiter ✔
└─ TwoSumTest ✔
└─ twoSum(int[], int, int[]) ✔
├─ [1] [1, 1], 2, [0, 1] ✔
├─ [2] [3, 2, 3], 6, [0, 2] ✔
└─ [3] [2, 7, 11, 15], 9, [0, 1] ✔
Test run finished after 219 ms
这种表达非常简单明了,容易定位问题。
总结 ¶
本文介绍的CsvSource和CsvFileSource,虽然细节上有些差异,但本质上是一回事。 在实际使用中,CsvFileSource可能更实用些。 单独的csv文件,更容易用规范的方法来写入(如Excel)或生成(如Python)。
而类似CsvSource的形式,优点是简单易用。
但还有更简单易用的办法,如ValueSource,直接把类型明确的测试case写在注解中。
此外,还有EnumSource,通过额外写一个enum来携带数据;或MethodSource,写一个Method来生成数据。
这三种形式,和CsvSource的写法非常类似,本文不再赘述。
(可以参考官方文档,或《JUnit 5 - Parameterized Tests - blog@CodeFX》。)
凡是需要参数化测试的地方,说明测试case数量不少,而且会需要随时增加。 比较下来,CsvFileSource才是最合适的。 而其它形式,尤其是ValueSource,可能更适合在快速开发的过程中,临时用一下。