9.3 测试Lambda表达式

现在你的代码中已经充溢着Lambda表达式,看起来不错,也很简洁。但是,大多数时候,我们受雇进行的程序开发工作的要求并不是编写优美的代码,而是编写正确的代码。

通常而言,好的软件工程实践一定少不了单元测试,借此保证程序的行为与预期一致。你编写测试用例,通过这些测试用例确保你代码中的每个组成部分都实现预期的结果。比如,图形应用的一个简单的Point类,可以定义如下:

  1. public class Point{
  2. private final int x;
  3. private final int y;
  4. private Point(int x, int y) {
  5. this.x = x;
  6. this.y = y;
  7. }
  8. public int getX() { return x; }
  9. public int getY() { return y; }
  10. public Point moveRightBy(int x){
  11. return new Point(this.x + x, this.y);
  12. }
  13. }

下面的单元测试会检查moveRightBy方法的行为是否与预期一致:

  1. @Test
  2. public void testMoveRightBy() throws Exception {
  3. Point p1 = new Point(5, 5);
  4. Point p2 = p1.moveRightBy(10);
  5. assertEquals(15, p2.getX());
  6. assertEquals(5, p2.getY());
  7. }

9.3.1 测试可见Lambda函数的行为

由于moveRightBy方法声明为public,测试工作变得相对容易。你可以在用例内部完成测试。但是Lambda并无函数名(毕竟它们都是匿名函数),因此要对你代码中的Lambda函数进行测试实际上比较困难,因为你无法通过函数名的方式调用它们。

有些时候,你可以借助某个字段访问Lambda函数,这种情况,你可以利用这些字段,通过它们对封装在Lambda函数内的逻辑进行测试。假设你在Point类中添加了静态字段comparByXAndThenY,通过该字段,使用方法引用你可以访问Comparator对象:

  1. public class Point{
  2. public final static Comparator<Point> compareByXAndThenY =
  3. comparing(Point::getX).thenComparing(Point::getY);
  4. ...
  5. }

还记得吗,Lambda表达式会生成函数接口的一个实例。由此,你可以测试该实例的行为。这个例子中,我们可以使用不同的参数,对Comparator对象类型实例compareByXAndThenYcompare方法进行调用,验证它们的行为是否符合预期:

  1. @Test
  2. public void testComparingTwoPoints() throws Exception {
  3. Point p1 = new Point(10, 15);
  4. Point p2 = new Point(10, 20);
  5. int result = Point.compareByXAndThenY.compare(p1 , p2);
  6. assertTrue(result < 0);
  7. }

9.3.2 测试使用Lambda的方法的行为

但是Lambda的初衷是将一部分逻辑封装起来给另一个方法使用。从这个角度出发,你不应该将Lambda表达式声明为public,它们仅是具体的实现细节。相反,我们需要对使用Lambda表达式的方法进行测试。比如下面这个方法moveAllPointsRightBy

  1. public static List<Point> moveAllPointsRightBy(List<Point> points, int x){
  2. return points.stream()
  3. .map(p -> new Point(p.getX() + x, p.getY()))
  4. .collect(toList());
  5. }

我们没必要对Lambda表达式p -> new Point(p.getX() + x,p.getY())进行测试,它只是moveAllPointsRightBy内部的实现细节。我们更应该关注的是方法moveAllPointsRightBy的行为:

  1. @Test
  2. public void testMoveAllPointsRightBy() throws Exception{
  3. List<Point> points =
  4. Arrays.asList(new Point(5, 5), new Point(10, 5));
  5. List<Point> expectedPoints =
  6. Arrays.asList(new Point(15, 5), new Point(20, 5));
  7. List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
  8. assertEquals(expectedPoints, newPoints);
  9. }

注意,在上面的单元测试中,Point类恰当地实现equals方法非常重要,否则该测试的结果就取决于Object类的默认实现。

9.3.3 将复杂的Lambda表达式分为不同的方法

可能你会碰到非常复杂的Lambda表达式,包含大量的业务逻辑,比如需要处理复杂情况的定价算法。你无法在测试程序中引用Lambda表达式,这种情况该如何处理呢?一种策略是将Lambda表达式转换为方法引用(这时你往往需要声明一个新的常规方法),9.1.3节详细讨论过这种情况。这之后,你可以用常规的方式对新的方法进行测试。

9.3.4 高阶函数的测试

接受函数作为参数的方法或者返回一个函数的方法(所谓的“高阶函数”,higher-order function,第19章会深入展开介绍)更难测试。如果一个方法接受Lambda表达式作为参数,那么你可以采用的一个方案是使用不同的Lambda表达式对它进行测试。比如,你可以使用不同的谓词对第2章中创建的filter方法进行测试。

  1. @Test
  2. public void testFilter() throws Exception{
  3. List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
  4. List<Integer> even = filter(numbers, i -> i % 2 == 0);
  5. List<Integer> smallerThanThree = filter(numbers, i -> i < 3);
  6. assertEquals(Arrays.asList(2, 4), even);
  7. assertEquals(Arrays.asList(1, 2), smallerThanThree);
  8. }

如果被测试方法的返回值是另一个方法,该如何处理呢?你可以仿照之前处理Comparator的方法,把它当成一个函数接口,对它的功能进行测试。

然而,事情可能不会一帆风顺,你的测试可能会返回错误,报告说你使用Lambda表达式的方式不对。因此,我们现在进入调试的环节。