Java 8 Lambdas的单元测试

Jam*_*wiy 5 testing lambda mocking java-8

我想知道是否有人找到一种方法来存储/模拟lambda中的逻辑而不会使lambda的可见性?

public List<Item> processFile(String fileName) {
   // do some magic..
   Function<String, List<String>> reader = (fileName) -> {
       List<String> items = new ArrayList<>();
       try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
          String output;
          while ((output = br.readLine()) != null) {
             items.add(output);
          }
       } catch (IOException e) {
          e.printStackTrace();
       }
     return items;
   };

   List<String> lines = reader.apply("file.csv");
   // do some more magic..
}
Run Code Online (Sandbox Code Playgroud)

Stu*_*rks 9

我会说规则是如果一个lambda表达式如此复杂以至于你觉得需要模拟它的一些部分,那它可能太复杂了.它应该被分解成一起组成的小块,或者可能需要调整模型以使其更适合于组合.

我会说Andrey Chaschev的答案表明参数化一个依赖是一个很好的,可能适用于某些情况.所以,+1为此.一个可以继续这一进程,并分解加工成更小的碎片,就像这样:

public List<Item> processFile(
    String fileName,
    Function<String, BufferedReader> toReader,
    Function<BufferedReader, List<String>> toStringList,
    Function<List<String>, List<Item>> toItemList)
{
    List<String> lines = null;
    try (BufferedReader br = toReader.apply(fileName)) {
        lines = toStringList.apply(br);
    } catch (IOException ioe) { /* ... */ }

    return toItemList.apply(lines);
}
Run Code Online (Sandbox Code Playgroud)

不过,有几点意见.首先,这不起作用,因为各种lambdas抛出讨厌的东西IOExceptions,它们被检查,并且Function没有声明类型抛出该异常.第二个是你必须传递给这个函数的lambdas是怪异的.即使这不起作用(因为检查异常)我写出来了:

void processAnActualFile() {
    List<Item> items = processFile(
        "file.csv",
        fname -> new BufferedReader(new FileReader(fname)),
                // ERROR: uncaught IOException
        br -> {
            List<String> result = new ArrayList<>();
            String line;
            while ((line = br.readLine()) != null) {
                result.add(line);
            }
            return result;
        },      // ERROR: uncaught IOException
        stringList -> {
            List<Item> result = new ArrayList<>();
            for (String line : stringList) {
                result.add(new Item(line));
            }
            return result;
        });
}
Run Code Online (Sandbox Code Playgroud)

啊! 我想我发现了新的代码味道:

如果你必须在lambda中写一个for循环或while循环,你就会做错事.

这里发生了一些事情.首先,I/O库实际上是由不同件实现(的InputStream,Reader,BufferedReader),它们紧耦合.尝试将它们分开是没有用的.实际上,该库已经发展,因此有一些便利工具(例如NIO Files.readAllLines)可以为您处理大量的腿部工作.

更重要的一点是,设计在它们之间传递值的聚合(列表)并组成这些函数的函数,实际上是错误的方法.它导致每个函数必须在其中编写循环.我们真正想做的是编写每个操作单个值的函数,然后让Java 8中的新Streams库为我们处理聚合.

从评论所描述的代码中提取的关键功能"做一些更多的魔法"转换List<String>List<Item>.我们想要提取将一个 转换String为一个的计算Item,如下所示:

class Item {
    static Item fromString(String s) {
        // do a little bit of magic
    }
}
Run Code Online (Sandbox Code Playgroud)

一旦你有了这个,那么你可以让Streams和NIO库为你做一堆工作:

public List<Item> processFile(String fileName) {
    try (Stream<String> lines = Files.lines(Paths.get(fileName))) {
        return lines.map(Item::fromString)
                    .collect(Collectors.toList());
    } catch (IOException ioe) {
        ioe.printStackTrace();
        return Collections.emptyList();
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意,这个简短方法的一半用于处理IOException.)

现在,如果你想进行一些单元测试,那么你真正需要测试的是一点魔力.所以你将它包装到不同的流管道中,如下所示:

void testItemCreation() {
    List<Item> result =
        Arrays.asList("first", "second", "third")
              .stream()
              .map(Item::fromString)
              .collect(Collectors.toList());
    // make assertions over result
}
Run Code Online (Sandbox Code Playgroud)

(实际上,即使这样也不是很正确.你想要编写将单行转换为单行的单元测试Item.但也许你在某处有一些测试数据,所以你可以用这种方式将它转换成项目列表,然后对列表中结果项的关系进行全局断言.)


我离你最初的如何分裂lambda的问题走了很远.请原谅我放纵自己.

原始示例中的lambda非常不幸,因为Java I/O库非常繁琐,并且NIO库中有新的API可以将示例转换为单行.

尽管如此,这里的教训是,不是组合处理聚合的函数,而是组合处理单个值的函数,并让流处理聚合.这样,您可以通过以不同方式将流管道插入在一起来进行测试,而不是通过模拟复杂lambda的位来进行测试.