lastminute.com logo

Technology

The power of functional interfaces in test

manuel_mandatori
manuel mandatori

How to use functional interfaces to mock behaviours in tests without implementing classes


Hello, everyone!

In this article, you will find part of the journey of a TDD enthusiast that likes to push the test to the next level. From the very beginning, every time I wrote a test in Java with a TDD approach, I usually import some mocking libraries some mock libraries such as Mockito or JMock. I use them to mock behaviors in collaborators of the class under test with the library syntax and reflecting very often the production code. Let’s see an example with Mockito framework about the mocking of collaborators where a class will call two repositories to retrieve some data. In this example we have a simple RetrieveProductsUseCase that needs hotel and flights (to stay in the same business) from HotelsRepository and FlightsRepository:

import org.junit.Before;
import org.junit.Test;

import java.util.List;

import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class SimpleTest {

    private FlightsRepository flightsRepository = mock(FlightsRepository.class);
    private HotelsRepository hotelsRepository = mock(HotelsRepository.class);

    private RetrieveProductsUseCase underTest;

    @Before
    public void setUp() {
        underTest = new RetrieveProductsUseCase(flightsRepository, hotelsRepository);
    }

    @Test
    public void name() {
        long idBooking = 123456789L;
        when(flightsRepository.getFor(idBooking)).thenReturn(new Flight());
        when(hotelsRepository.getFor(idBooking)).thenReturn(new Hotel());
        List<Product> products = underTest.retrieveFor(123456789L);
        assertThat(products, hasSize(2));
    }

    private class RetrieveProductsUseCase {

        private FlightsRepository flightsRepository;
        private HotelsRepository hotelsRepository;

        public RetrieveProductsUseCase(
                FlightsRepository flightsRepository,
                HotelsRepository hotelsRepository) {

            this.flightsRepository = flightsRepository;
            this.hotelsRepository = hotelsRepository;
        }

        public List<Product> retrieveFor(long idBooking) {
            return asList(flightsRepository.getFor(idBooking), hotelsRepository.getFor(idBooking));
        }
    }

    private interface FlightsRepository {
        Flight getFor(long idBooking);
    }

    private interface HotelsRepository {
        Hotel getFor(long idBooking);
    }

    private class Product {}

    private class Hotel extends Product {}

    private class Flight extends Product {}

}

From the example above, the two repositories contain only one method to retrieve the products. Why not using the FunctionalInterface annotation? This handy annotation was introduced in Java 8 and defines an interface as a functional interface when it contains only one abstract method (Oracle reference: [Functional Interface] https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html). Thanks to this annotation we can substitute the implementation of the interface with a lambda expression:


public class SimpleTest {
    @Test
    public void name() {
        long idBooking = 123456789L;
        flightsRepository = () -> new Flight();
        hotelsRepository = () -> new Hotel();

        List<Product> products = underTest.retrieveFor(123456789L);
        assertThat(products, hasSize(2));

    }

    @FunctionalInterface
    private interface FlightsRepository {
        Flight getFor(long idBooking);
    }

    @FunctionalInterface
    private interface HotelsRepository {
        Hotel getFor(long idBooking);
    }

}

Now the test is more clear and there is no concern about the implementation of the two repositories. The usage of FunctionalInterface allows the developer to focus on testing the functionality without the constraint to create dummy implementations.

Another important feature of the mocking libraries is checking on the input parameters. How could we do the same inside the anonymous function? With assertions :D Let’s take the example and change it to be sure that the idBooking passed to the repository is the same that we use as input to the use case:


public class SimpleTest {
    @Test
    public void name() {
        long idBooking = 123456789L;
        flightsRepository = (inputIdBooking) -> {
            assertEquals(inputIdBooking, idBooking);
            return new Flight();
        };
        hotelsRepository = () -> {
            assertEquals(inputIdBooking, idBooking);
            return new Hotel();
        };

        List<Product> products = underTest.retrieveFor(123456789L);
        assertThat(products, hasSize(2));

    }

    @FunctionalInterface
    private interface FlightsRepository {
        Flight getFor(long idBooking);
    }

    @FunctionalInterface
    private interface HotelsRepository {
        Hotel getFor(long idBooking);
    }

}

Now we are sure that when the input value is not the wanted one, the test will fail in the same way it would with mocking libraries.

After these examples, we could have the last question: What is missing when you use FunctionalInterface instead of mocks? The response is: the number of calls verification, which must be done manually. The verify method of Mockito accepts a parameter that corresponds to the number of calls expected. With the lambda function, you should create a counter variable and increment it every time the mock is called.


Read next

Evolution of a high-performance system: from synchronous to seamless scalability

Evolution of a high-performance system: from synchronous to seamless scalability

giuseppe_pinto
giuseppe pinto

This article discusses the transformation of a synchronous process in the context of lastminute.com's customer journey for travel planning. The existing system was complex and resource-intensive due to its all-in-one nature, causing scaling and deployment challenges. The article presents the evolved system that separates responsibilities into microservices using message-driven communication. [...]

Lambda Integration with MSK and KrakenD

Lambda Integration with MSK and KrakenD

otto_abreu
otto abreu

API Gateways can be a simple way to expose asynchronous communication without revealing too many details about it but, is not always an easy task to implement. In this post, I explore a way to expose Kafka topics in a well-known API Gateway (KrakenD) while circumventing multiple limitations. [...]