1. 概述

在Java中,继承是一个重要的概念,而接口是实现这一概念的一种方式。接口定义了一个契约,多个类可以实现它。因此,确保这些实现类遵循相同的规范至关重要。

本教程将探讨如何使用JUnit测试为Java接口编写不同方法。

2. 准备工作

首先,我们创建一个简单的名为Shape的接口,它有一个area()方法:

public interface Shape {

    double area();
}

接下来,定义一个Circle类,它实现了Shape接口,并且有自己特有的circumference()方法:

public class Circle implements Shape {

    private double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return 3.14 * radius * radius;
    }

    public double circumference() {
        return 2 * 3.14 * radius;
    }
}

最后,定义另一个类Rectangle,它也实现了Shape接口,还新增了一个perimeter()方法:

public class Rectangle implements Shape {

    private double length;
    private double breadth;

    public Rectangle(double length, double breadth) {
        this.length = length;
        this.breadth = breadth;
    }

    @Override
    public double area() {
        return length * breadth;
    }

    public double perimeter() {
        return 2 * (length + breadth);
    }
}

3. 测试方法

现在让我们看看可以采用的不同测试策略。

3.1. 实现类的独立测试

一种常见的做法是为接口的每个实现类创建单独的JUnit测试类。我们会测试每个类继承的接口方法以及它们自身定义的方法。

首先,我们创建CircleUnitTest类,针对area()circumference()方法编写测试用例:

@Test
void whenAreaIsCalculated_thenSuccessful() {
    Shape circle = new Circle(5);
    double area = circle.area();
    assertEquals(78.5, area);
}

@Test
void whenCircumferenceIsCalculated_thenSuccessful(){
    Circle circle = new Circle(2);
    double circumference = circle.circumference();
    assertEquals(12.56, circumference);
}

接着,我们创建RectangleUnitTest类,针对area()perimeter()方法编写测试用例:

@Test
void whenAreaIsCalculated_thenSuccessful() {
    Shape rectangle = new Rectangle(5,4);
    double area = rectangle.area();
    assertEquals(20, area);
}

@Test
void whenPerimeterIsCalculated_thenSuccessful() {
    Rectangle rectangle = new Rectangle(5,4);
    double perimeter = rectangle.perimeter();
    assertEquals(18, perimeter);
}

如上述两个类所示,我们可以成功地测试接口方法以及实现类可能定义的额外方法。

通过这种方法,我们可能需要为接口方法在所有实现类中重复编写相同的测试。例如,我们在两个实现类中都测试了area()方法。

随着实现类数量的增长,由于接口定义的方法数量增加,测试用例也会成倍增长。这会导致代码复杂性和冗余性增加,随着时间的推移维护和修改变得困难。

3.2. 参数化测试

为了解决这个问题,我们可以创建一个参数化测试,它接受不同实现类的实例作为输入:

@ParameterizedTest
@MethodSource("data")
void givenShapeInstance_whenAreaIsCalculated_thenSuccessful(Shape shapeInstance, double expectedArea){
    double area = shapeInstance.area();
    assertEquals(expectedArea, area);
}

private static Collection<Object[]> data() {
    return Arrays.asList(new Object[][] {
      { new Circle(5), 78.5 },
      { new Rectangle(4, 5), 20 }
    });
}

这样,我们已经成功地验证了接口对实现类的合同要求。

然而,我们无法在接口定义之外定义任何其他内容。因此,可能仍需要以某种形式测试实现类。这可能需要在它们自己的JUnit类中进行测试。

3.3. 使用基类测试

前两种方法缺乏足够的灵活性来扩展测试用例,除了验证接口合同外。同时,我们也希望避免代码冗余。接下来,我们将探讨另一种可以解决这两个问题的方法。

在这种方法中,我们定义一个基础测试类。这个抽象的测试类定义了要测试的方法,即接口的合同。随后,实现类的测试类可以扩展这个抽象测试类,以增强测试。

我们将使用模板方法模式,在基础测试类中定义测试area()方法的算法,然后子类只需要提供要在算法中使用的实现即可。

首先,定义基础测试类来测试area()方法:

public abstract Map<String, Object> instantiateShapeWithExpectedArea();

@Test
void givenShapeInstance_whenAreaIsCalculated_thenSuccessful() {
    Map<String, Object> shapeAreaMap = instantiateShapeWithExpectedArea();
    Shape shape = (Shape) shapeAreaMap.get("shape");
    double expectedArea = (double) shapeAreaMap.get("area");
    double area = shape.area();
    assertEquals(expectedArea, area);
}

接下来,创建Circle类的JUnit测试类:

@Override
public Map<String, Object> instantiateShapeWithExpectedArea() {
    Map<String,Object> shapeAreaMap = new HashMap<>();
    shapeAreaMap.put("shape", new Circle(5));
    shapeAreaMap.put("area", 78.5);
    return shapeAreaMap;
}

@Test
void whenCircumferenceIsCalculated_thenSuccessful(){
    Circle circle = new Circle(2);
    double circumference = circle.circumference();
    assertEquals(12.56, circumference);
}

最后,创建Rectangle类的测试类:

@Override
public Map<String, Object> instantiateShapeWithExpectedArea() {
    Map<String,Object> shapeAreaMap = new HashMap<>();
    shapeAreaMap.put("shape", new Rectangle(5,4));
    shapeAreaMap.put("area", 20.0);
    return shapeAreaMap;
}

@Test
void whenPerimeterIsCalculated_thenSuccessful() {
    Rectangle rectangle = new Rectangle(5,4);
    double perimeter = rectangle.perimeter();
    assertEquals(18, perimeter);
}

在这个方法中,我们重写了instantiateShapeWithExpectedArea()方法。在这个方法中,我们提供了Shape实例和预期的面积。这些参数可以被基础测试类中的测试方法用来执行测试。

总结来说,通过这种方法,实现类可以为其自身方法编写测试,并继承接口方法的测试。

4. 结论

在这篇文章中,我们探讨了验证接口合同的不同JUnit测试方法。

首先,我们看到了为每个实现类单独创建测试类的直接性,但这也可能导致大量冗余代码。

然后,我们研究了如何使用参数化测试来避免冗余,但其灵活性较低。

最后,我们看到了基类测试方法,它解决了前两种方法中的问题。

如往常一样,源代码可以在GitHub上找到。