JUnit 5 - 将多个 CSV 文件参数转换为单个对象

JUnit 5 - Convert multiple CSV file parameters into single object

你好 Java 测试人员。 几天前我开始使用 JUnit 5,因为我喜欢创建参数化测试的新方法。

@ParameterizedTest 允许以参数化方式测试 运行,@CsvFileSource 从 CSV 文件加载参数。

问题是我的 CSV 中有太多列并且不想在我的单元测试中有一个巨大的方法签名。让我举个例子:

@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv")
void myTest(String p1, String p2, String p3, String p4, String p5, String p6) {
    //test using parameters
}

我想知道是否有某种转换器可以为我做这样的事情:

@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv")
void myTest(@ConvertWith(TestDataConverter.class) TestData testData) {
    //test using parameters
}

static class TestDataConverter implements TestConverter{
    public TestData convert(Object ... params){
       //a simple method that creates the TestData object and inserts the params in it
    }
}

就是这样。

更新答案

从 JUnit Jupiter 5.2 开始:,有专门的 ArgumentsAggregatorArgumentsAccessor API 正是用于此目的。

查看 JUnit 用户指南中的 PersonAggregator 示例以获得具体示例。

原答案

从 JUnit Jupiter 5.0 开始:不,目前没有开箱即用的转换器可以为您做到这一点。

原因:JUnit Jupiter 中的参数化测试支持不支持从多个参数到单个参数的映射。因此,正如@Sormuras 所建议的那样,您可以打开一个问题来推荐它。

就执行实际转换而言,您可以考虑使用 uniVocity-parsers,JUnit Jupiter 在内部使用它来解析 CSV 文件。 uniVocity-parsers 还支持直接从 CSV 文件映射到 beans,但要使用它,您需要实现自己的 @TestTemplate 来读取 CSV 文件并执行映射。

其他选项包括来自 Jackson framework or from the JSefa project 的 CSV 支持。

不是通用的解决方案,而是一个有时可行的小技巧:

  • 将 CSV 文件的每一行括在引号之间...以便每一行都被视为一个字符串(sedawk 或任何类似的工具都是您的朋友)
  • 现在实现 TestConverter 以便将单个字符串转换为 String[] 以供您随意使用。在许多情况下,它可以像这样简单:

    static class TestDataConverter implements TestConverter{
      public TestData convert(String string){
    
      String[] params = string.split(",");
    
      //a simple method that creates the TestData object and inserts the params in it
      }
    }
    

更新:

一个非常常见的不允许直接使用该技巧的情况是,CSV 中已经使用引号来处理字段内的 'comma':

Name, complex field, City
Carlo, "this field could contain , and other critical chars" , London
Mario, "this field could contain , and other critical chars" , New York
Luca, "this field could contain , and other critical chars" , Milan

用相同的引号引用行显然不起作用。

不过,这种情况或类似情况可以通过对 CSV 文件进行一些额外(但无聊)的转换来解决。

例如假设3个连续的升号“###”可以被认为是一个一致的分​​隔符:

"Name###complex field###City"
"Carlo###this field could contain , and other critical chars###London"
"Mario###this field could contain , and other critical chars###New York"
"Luca###this field could contain , and other critical chars###Milan"

我知道...这是一个技巧..不是一个明确而优雅的解决方案...:-D

很明显,在这种情况下,拆分将在 3 个哈希值上进行:

String[] params = string.split("###");

您可以使用这个聚合器,尽管有点老套:

public class CsvToMapAggregator implements ArgumentsAggregator {

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    @AggregateWith(CsvToMapAggregator.class)
    public @interface CsvToMap {}

    private static String[] keys = null; // ATTN: Do not use in parallel runs !!!

    @Override public Map<String, Object> aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
        Object[] values = accessor.toArray();
        if (keys == null) {
            keys = (String[]) values; // First Line: Header
        } else if (values.length != keys.length) {
            keys = null; // Last Line: End of Data
            return null;
        }
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < values.length; i++) {
            map.put(keys[i], values[i]); // Read Data from Line
        }
        return map;
    }
}

注意: 所有行必须具有相同数量的项目。

csv 文件必须以额外的行结尾(例如第一列中的"EOF"),该行的数量与前面的行不同。

测试方法将如下所示,跳过 header 和结束行:

@ParameterizedTest(name = "{0}")
@CsvFileSource(resources = "/testcases.csv", delimiter = ';')
@DisplayName("read complete .csv and map columns")
void testCsvData(@CsvToMap Map<String, Object> map, TestInfo info) {
    switch (info.getDisplayName()) {
        case "Testcase": // First Line: Header
        case "EOF": // Last Line: End of File
            break;
        default:
            assertNotNull(map);
            assertFalse(map.isEmpty());
}