Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

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

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.

2. Setup

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():

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:

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():

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

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

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.

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

@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:

@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.

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

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.

3.3. Using a Base Test Class

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.

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

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:

@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:

@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.

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

4. Conclusion

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

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.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!