Testing Interface Contract in Java – 测试 Java 中的接口合同

最后修改: 2023年 10月 3日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Inheritance is an important concept in Java. Interfaces are one of the ways through which we implement the concept.

继承是 Java 中的一个重要概念。 接口是我们实现这一概念的方法之一。

Interfaces define a contract that multiple classes can implement. Subsequently, it’s essential to test these implementing classes to ensure they adhere to the same.

接口定义了一个契约,多个类都可以实现该契约。因此,必须对这些实现类进行测试,以确保它们遵守相同的契约。

In this tutorial, we’ll take a look at different approaches to writing JUnit tests for interfaces in Java.

在本教程中,我们将介绍为 Java 中的接口编写 JUnit 测试的不同方法。

2. Setup

2.设置

Let’s create a basic setup to use in our different approaches.

让我们创建一个基本设置,用于我们的不同方法。

Firstly, we start by creating a simple interface called Shape, which has a method area():

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

public interface Shape {

    double area();
}

Secondly, we define a Circle class that implements the Shape interface. It also has a method circumference() of its own:

其次,我们定义了一个实现 Shape 接口的 Circle 类。它也有一个自己的方法 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;
    }
}

Lastly, we define another class, Rectangle, that implements the Shape interface. It has an additional method perimeter():

最后,我们定义了另一个实现 Shape 接口的类 Rectangle, 。它有一个额外的方法 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. Test Approaches

3.测试方法

Now, let’s take a look at the different approaches we can follow to test the implementing classes.

现在,让我们来看看测试实现类的不同方法。

3.1. Individual Tests for Implementing Classes

3.1.实现类的单项测试

One of the most popular approaches is to create individual JUnit test classes for each implementation class of the interface. We’ll test both the methods for the classes –  the one inherited as well as the one defined by the class itself.

最常用的方法之一是为接口的每个实现类创建单独的 JUnit 测试类。我们将同时测试类的方法–继承的方法和类本身定义的方法。

Initially, we create the CircleUnitTest class, with test cases for area() and circumference() methods:

最初,我们创建了 CircleUnitTest 类,其中包含 area()ircumference() 方法的测试用例:

@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);
}

In the next step, we create the RectangleUnitTest class with test cases for the area() and perimeter() methods:

下一步,我们将创建 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);
}

As we can see from both the classes above, we can successfully test the interface methods and any additional methods the implementing classes may define. 

从上述两个类中我们可以看到,我们可以成功地测试接口方法和实现类可能定义的任何其他方法。

With this approach, we may have to write the same test for the interface methods repeatedly for all the implementing classes. As we see with individual tests, the same area() method is being tested in the two implementing classes.

使用这种方法,我们可能不得不为所有实现类的接口方法重复编写相同的测试。正如我们在单个测试中看到的,两个实现类正在测试同一个 area() 方法。

As the number of implementing classes grows, the tests are multiplied across implementations with the increase in the number of methods defined by the interface. Consequently, the code complexity and redundancy grow as well, making it difficult to maintain and change over time.

随着实现类数量的增加,测试也会随着接口定义的方法数量的增加而成倍增加。因此,代码的复杂性和冗余性也随之增加,使其难以维护和随时间变化

3.2. Parameterized Tests

3.2.参数化测试

To overcome this, let’s create a parameterized test, which takes as input the instances of the different implementation classes:

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

@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 }
    });
}

With this approach, we’ve successfully tested the interface contract for the implementing classes.

通过这种方法,我们成功地测试了实现类的接口契约。

However, we don’t have the flexibility to define anything additional than what has been defined in the interface. Hence, we may still need to test the implementing classes in some other form. It may require testing them in their own JUnit classes.

但是,我们无法灵活地定义接口定义以外的任何内容。因此,我们可能仍然需要以其他形式测试实现类。这可能需要在它们自己的 JUnit 类中测试它们。

3.3. Using a Base Test Class

3.3.使用基本测试类

With the previous two approaches, we don’t have enough flexibility to extend the test cases in addition to verifying the interface contract. At the same time, we also want to avoid code redundancy. So, let’s look at another approach that can address both concerns.

采用前两种方法时,我们没有足够的灵活性,除了验证接口契约外,还需要扩展测试用例。同时,我们还希望避免代码冗余。因此,我们来看看另一种能同时解决这两个问题的方法。

In this approach, we define a base test class. This abstract test class defines the methods to be tested, i.e., the interface contract. Subsequently, the test classes of the implementing classes can extend this abstract test class to build upon the tests.

在这种方法中,我们定义了一个基础测试类。这个抽象测试类定义了要测试的方法,即接口契约。随后,实现类的测试类可以扩展这个抽象测试类,在此基础上进行测试。

We’ll be using the template method pattern wherein we define the algorithm to test the area() method in the base test class, and then, the test sub-classes are only required to provide the implementations to be used in the algorithm.

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

Let’s define the base test class to test the area() method:

让我们定义基础测试类来测试 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);
}

Now, let’s create the JUnit test class for the Circle class:

现在,让我们为 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);
}

Finally, the test class for the Rectangle class:

最后,是 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);
}

In this approach, we’ve overridden the instantiateShapeWithExpectedArea() method. In this method, we’ve provided the Shape instance as well as the expected area. These parameters can be used by the test methods defined in the base test class to execute the tests.

在这种方法中,我们重载了 instantiateShapeWithExpectedArea() 方法。在该方法中,我们提供了 Shape 实例和预期区域。基础测试类中定义的测试方法可以使用这些参数来执行测试。

To summarize, with this approach, implementing classes can have tests for their own methods and inherit the tests for the interface methods.

总之,采用这种方法,实现类可以为自己的方法设置测试,并继承接口方法的测试。

4. Conclusion

4.结论

In this article, we explored the different ways of writing JUnit tests to validate the interface contract.

在本文中,我们探讨了编写 JUnit 测试以验证接口契约的不同方法。

First, we took a look at how defining individual test classes for each implementing class is straightforward. However, this may lead to a lot of redundant code.

首先,我们了解了为每个实现类定义单独的测试类是如何简单易行的。然而,这可能会导致大量冗余代码。

Then, we explored how using parameterized tests can help us avoid redundancy, but it’s less flexible.

然后,我们探讨了使用参数化测试如何帮助我们避免冗余,但灵活性较低。

Finally, we saw the base test class approach, which addresses the concerns in the other two approaches.

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

As always, the source code is available over on GitHub.

与往常一样,源代码可在 GitHub 上获取