Search

Dark theme | Light theme

March 25, 2024

Mastering Mockito: Returning Fresh Stream For Multiple Calls To Mocked Method

When we mock a method that returns a Stream we need to make sure we return a fresh Stream on each invocation to support multiple calls to the mocked method. If we don’t do that, the stream will be closed after the first call and subsequent calls will throw exceptions. We can chain multiple thenReturn calls to return a fresh Stream each time the mocked method is invoked. Or we can use multiple arguments with the thenReturn method, where each argument is returned based on the number of times the mocked method is invoked. So on the first invocation the first argument is returned, on second invocation the second argument and so on. This works when we know the exact number of invocations in advance. But if we want to be more flexible and want to support any number of invocations, then we can use thenAnswer method. This method needs an Answer implementation that returns a value on each invocation. The Answer interface is a functional interface with only one method that needs to be implemented. We can rely on a function call to implement the Answer interface where the function gets a InvocationOnMock object as parameter and returns a value. As the function is called each time the mocked method is invoked, we can return a Stream that will be new each time.

In the following example we use chained method calls using thenReturn and we use thenAnwer to support multiple calls to our mocked method temperature that returns a Stream of Double values:

package mrhaki;

import org.junit.jupiter.api.Test;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockReturnMultipleStreams {

    // Simple interface to return a stream of
    // temperature values for a given city.
    interface Weather {
        Stream<Double> temperature(String city);
    }

    // Simple class that uses Weather interface.
    static class SubjectUnderTest {

        private final Weather weather;

        SubjectUnderTest(Weather weather) {this.weather = weather;}

        public String weatherReport(String city) {
            // By invoking the methods celcius and fahrenheit we will
            // invoke the weather.temperature method twice.
            return String.format("The temperature in %s is %.1f degrees Celcius or %.1f degrees Fahrenheit.",
                                 city, celcius(city), fahrenheit(city));
        }

        private double celcius(String city) {
            return weather.temperature(city).findAny().get();
        }

        private double fahrenheit(String city) {
            return (celcius(city) * 9/5)  + 32;
        }
    }

    private final Weather weather = mock(Weather.class);
    private final SubjectUnderTest subjectUnderTest = new SubjectUnderTest(weather);

    @Test
    void shouldReturnCorrectWeatherReport() {
        // given

        // Return type of the mocked method temperature is a Stream.
        // On the first call in the subjectUnderTest instance the stream
        // is closed, so the second call will give an exception that
        // the stream is already been operated upon or closed.
        // To support the second call we need to return a new stream
        // with the same content.
        // If we need to support more calls than two we need
        // to add more thenReturn statements.
        // See the next test method for an example with thenAnswer
        // that supports multiple calls more easily.
        double temperature = 21.0;
        when(weather.temperature("Tilburg"))
                // First call
                .thenReturn(Stream.of(temperature))
                // Second call
                .thenReturn(Stream.of(temperature));

        // Alternative syntax:
        // when(weather.temperature("Tilburg"))
        //        .thenReturn(Stream.of(temperature), Stream.of(temperature));

        // when
        String result = subjectUnderTest.weatherReport("Tilburg");

        // then
        assertThat(result).isEqualTo("The temperature in Tilburg is 21,0 degrees Celcius or 69,8 degrees Fahrenheit.");
    }

    @Test
    void shouldReturnCorrectWeatherReport2() {
        // given

        // Return type of the mocked method temperature is a Stream.
        // On the first call in the subjectUnderTest instance the stream
        // is closed, so the second call will give an exception that
        // the stream is already been operated upon or closed.
        // To support the second call we can use thenAnswer method
        // which will return a fresh Stream on each call.
        // Now the number of calls is not limited, because on each
        // invocation a fresh Stream is created.
        when(weather.temperature("Tilburg"))
                .thenAnswer(invocationOnMock -> Stream.of(21.0));

        // when
        String result = subjectUnderTest.weatherReport("Tilburg");

        // then
        assertThat(result).isEqualTo("The temperature in Tilburg is 21,0 degrees Celcius or 69,8 degrees Fahrenheit.");
    }
}

Written with Mockito 3.12.4.