Interface Contract with JUnit 5

It is a common misconception among developers, that Java interfaces can be considered reasonable contracts for possible providers of the interface implementation. Although interfaces cover some vital parts of contracts, in my opinion, interfaces cannot be considered real contracts at all. Fortunately, JUnit 5 is there to cover your back with a new feature. This can be used to flesh out interfaces to become more like real contracts.
In recent times the concept of contracts and contract testing often appear in the context of testing the connection between different microservices using Spring Cloud Contract or PACT. This is a high-level approach tackling inter-service communication. In contrast interface contracts, the topic of this article, are located on a much smaller scale in software design targeting class and interface relationships which are part of an application or single microservice.
What makes an interface contract?
A real contract for a method consists at least of the following parts:
- method name
- parameters with types
- return type
- named preconditions with assertions
- named postconditions with assertions
If the contract affects a class, it additionally contains named invariants with assertions. It is necessary to understand, that all these parts need to be automatically checked somewhere in the development process. Although only writing a contract down may help in the development process, the contract quickly falls off when not automatically verified.
In general, when considering a class (not an interface) the parts of the contract are implemented by the class itself and its tests. Whereby the class takes care of the method names, parameters and return types and the test checks preconditions, postconditions and invariants. However, in most of the projects I visited this approach is not followed when creating interfaces. It seems that tests for interfaces are very uncommon not to mention tests for implementation classes verifying pre- and postconditions of their implemented interface methods.
A plain interface without any tests provides only the first three parts listed above, missing the crucial logical part of a contract. Why is this a problem? Consider the following interface:
1 2 3 |
public interface Sum { int add(int first, int second); } |
The intention of the interface is, that an implementation using it, sums up two given numbers. This is a postcondition. By the way, it is important to understand, that this postcondition makes no assumption on what the implementation is. Whether the sum is calculated using a for-loop, a library or basic language features is of no interest to the postcondition. However, based on only the interface itself the following implementation is totally valid even though the intention of the interface is not met:
1 2 3 4 5 6 |
public class SumTwo implements Sum { @Override public int add(int first, int second) { return 2; } } |
The reasons for such a misconception are among others the two basic points: 1. The postcondition is not communicated by only the interface itself. 2. There is no way to automatically check, that the intentions of the interface are met. Therefore and for various other reasons a plain interface, in my opinion, cannot be called a real contract.
To add a real-world example: If bringing your car to the workshop to get it repaired was a java interface, it would be perfectly reasonable to get back a totally different car and it does not even have to be working. Remember, only input parameter (your car) and output parameter („a car“) would be specified by the interface contract. I hope you would not consider this a contract in any way.
Though with an enhanced contract you gain the following benefits:
- clear expression of pre- and postconditions
- automatic check of logical contract conditions
- reduction of test code in implementing classes leading to more readability
The last point results from the circumstance, that the logical contract conditions can be checked by the interface contract and therefore do not need to be checked again and again in the tests of the implementation classes.
How to create an interface contract with JUnit 5?
With JUnit 5 a new feature was introduced making it possible to add tests to interfaces. These tests can be run with every implementation class of the interface almost out of the box. This mechanism can be used to create interface contracts.
Let’s examine the general structure. An in-practice example will follow in the next section. The overall structure looks like this:
At first you need an interface defining the declarative part of the contract:
1 2 3 |
interface Interface { String method(String parameter); } |
For this interface the logical contract part goes into another interface which is in the test source set of the project:
1 2 3 4 5 6 7 8 9 10 |
interface InterfaceContract { Interface create(); @Test default void postcondition() { String parameter = RandomStringUtils.random(12); assertThat(create().method(parameter)).isNotBlank(); } } |
Here are several things to pay attention to:
- Although the contract is in the test source set, it is not a test class but an interface itself.
- The test method checking the condition needs to be a default implementation of the interface method marked with the default keyword at the method declaration.
- To receive the implementation class the contract is checked on, a create() method has to be on the Contract. It returns the Interface the contract belongs to. How this is used will be shown in the next step.
- In the method checking the condition the create() method is used to retrieve the implementation class and access its implementation of the interface method.
That’s it for the interface part. The contract is now established. In the next steps an implementation is created using the interface and its contract.
As usual the implementation class uses the interface and implements its methods:
1 2 3 4 5 6 |
class InterfaceImplementation implements Interface { @Override public String method(String parameter) { return "content"; } } |
The contract then is used in the test for the implementation:
1 2 3 4 5 6 |
class InterfaceImplementationTest implements InterfaceContract { @Override public Interface create() { return new InterfaceImplementation(); } } |
For the test the following things are important:
- The test class needs to implement the interface contract
- The create() method has to be implemented and needs to return an instance of the implementation class. This mechanism assures that the contract is verified on the implementation class.
This finishes the implementation part. JUnit 5 will take care of the rest. Although no actual test is declared in the implementation class test, it can be run and will execute the contract check on the implementation class.
More sophisticated example
In the practical example an interface for arranging an appointment is shown. Arranging appointments sometimes is very annoying, so let’s automate this. How the appointment is arranged is up to different implementations. To underline my point of increasing readability and documentation, have a look at the Appointment interface contract on the right side. Can you guess, what the pre- and postconditions for arranging an appointment are? Could you have certainly said that these are the conditions only by looking at the interface itself?
Appointment interface
1 2 3 |
public interface Appointment { LocalDate arrange(Set<LocalDate> possibleDates); } |
Appointment interface contract
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
interface AppointmentContract { Appointment create(); LocalDate possibleDate(); @Test default void shouldThrowExceptionWithNull() { assertThatThrownBy(() -> create().arrange(null)) .isInstanceOf(IllegalArgumentException.class); } @Test default void shouldThrowExceptionWithEmptySet() { assertThatThrownBy(() -> create().arrange(Collections.emptySet())) .isInstanceOf(IllegalArgumentException.class); } @Test default void shouldReturnDateOnPossibleDate() { assertThat(create().arrange(Set.of(possibleDate()))) .isEqualTo(possibleDate()); } @Test default void shouldReturnNullWhenNoDateIsPossible() { assertThat(create().arrange(Set.of(randomDate(), randomDate()))) .isNull(); } private LocalDate randomDate() { // generates a random date } } |
From the Appointment interface contract one can clearly see the pre- and postconditions:
- passing null for the set of possible dates should raise an exception
- passing an empty set of possible dates should raise an exception as well
- when a possible date is found in the set of possible dates, it should be returned
- when no possible date is found, null should be returned
Notice that there is an additional possibleDate() helper method. This method returns a date which suits the algorithm of an implementation class as a fitting appointment date. Depending on the implementation this date may have to fulfill certain requirements. The implementation class test can then take care to craft a date fulfilling the requirements.
With the Appointment interface and its contract in place, let’s have a look at some implementations. First implementation is an algorithm allowing appointments only to be made on Fridays, because that’s the only day I’m in the office:
Appointment implementation allowing only appointments on fridays
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class OnlyOnFridayAppointment implements Appointment { @Override public LocalDate arrange(Set<LocalDate> possibleDates) { if(possibleDates == null) throw new IllegalArgumentException(); if(possibleDates.isEmpty()) throw new IllegalArgumentException(); return possibleDates.stream() .filter(date -> date.getDayOfWeek() == DayOfWeek.FRIDAY) .findFirst() .orElse(null); } } |
Test for OnlyOnFridayAppointment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class OnlyOnFridayAppointmentContract implements AppointmentContract { private LocalDate possibleDate = fridayDate(); @Override public Appointment create() { return new OnlyOnFridayAppointment(); } @Override public LocalDate possibleDate() { return possibleDate; } @Test void shouldReturnNoAppointmentWhenNoDateIsOnFriday() { assertThat(create().arrange(Set.of(noFridayDate(), noFridayDate()))) .isNull(); } @Test void shouldReturnAppointmentWhenDateIsOnFriday() { LocalDate friday = fridayDate(); assertThat(create().arrange(Set.of(noFridayDate(), friday))) .isEqualTo(friday); } public LocalDate fridayDate() { // Logic to generate a date which is on friday goes here } public LocalDate noFridayDate() { // Logic to generate a date which is not on friday goes here } } |
In the implementation there are two checks throwing an exception. These are enforced by the interface contract and therefore cannot be skipped. Everything else is straight forward.
In the tests there are several things to notice beside how the interface contract is introduced:
- Beside the interface contract there can be other test methods checking the behavior of the specific implementation.
- possibleDate() in this case returns a date tailored for this implementation. It fulfills the requirement, that only dates on a Friday are acceptable.
- create() may also be used in the implementation specific test methods to create the class under test
According to the Reuse Abstraction Principle there should be at least two implementations for an interface to make sense. So, here is a second implementation. Fulfilling a contract for an object-graph leaf implementation class seems to be rather easy. However, things tend to get complicated when additional dependencies come into play. Therefore, the second implementation comes with a dependency to demonstrate how to handle them:
Appointment implementation allowing appointments with marketing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class MarketingAppointment implements Appointment { private final MarketingCalender calender; public MarketingAppointment(MarketingCalender calender) { this.calender = calender; } @Override public LocalDate arrange(Set<LocalDate> possibleDates) { if(possibleDates == null) throw new IllegalArgumentException(); if(possibleDates.isEmpty()) throw new IllegalArgumentException(); Set<LocalDate> marketingDates = calender.possibleAppointmentDays(); return possibleDates.stream() .filter(marketingDates::contains) .findFirst() .orElse(null); } } |
Test for MarketingAppointment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class MarketingAppointmentTest implements AppointmentContract { private MarketingAppointment cut; private LocalDate possibleDate; @BeforeEach void setUp() { possibleDate = randomDate(); MarketingCalender calender = mock(MarketingCalender.class); cut = new MarketingAppointment(calender); when(calender.possibleAppointmentDays()) .thenReturn(Set.of(possibleDate)); } @Override public Appointment create() { return cut; } @Override public LocalDate possibleDate() { return possibleDate; } private LocalDate randomDate() { // Logic to generate a random date goes here } } |
This implementation takes care, that appointments are only made on days where the marketing department has time to join. This is because of the cute colleague from marketing I like to invite to my appointments. The interesting part in the implementation is, that it works with MarketingCalender as dependency. The dependency needs to be mocked in the test, which you can clearly see, how it’s done. In case of dependencies stubbing may be necessary to fulfill the conditions of the contract.
MarketingCalenders possibleAppointmentDays() returns only dates at which the marketing is free and can join the appointment.
An important point to acknowledge is that tests already defined in the contract do not need to be repeated in the implementation. Without the contract tests for checking that the possibleDates parameter is not null or empty would have to be added to each implementation class test, duplicating or copy-and-pasting code. In addition, this would have increased the size of the test file making it likely less readable.
Limitations
As already mentioned, the interface contract approach shown here works very well for leaves in the object-graph. This is because for non-leaves most likely further dependencies must be mocked in the implementation test class. Those dependencies may be crucial for writing the interface contract, making it hard to even create a meaningful pre- or postcondition.
When writing the test for an implementation class, the developer needs to know, that there is in fact an interface contract. In the current approach using the interface contract in the test for an implementation class is not enforced in any way. Due to this fact using the interface contract may easily be forgotten. Maybe an extension for a static code analysis tool could help which checks that every interface contract is used in the implementation class. However, no such extension exists at the time of writing this article.
Summary
With the new JUnit 5 features Java interfaces can finally become real contracts. This helps to check pre- and postconditions, reduce test code, increases documentation and readability. Using the feature is easy. Do not miss out on this improvement!
What do you think? Is this a great enhancement or just another overhead? Are there places in your project where interface contracts could be a help?
Aktuelle Beiträge




Artikel kommentieren