Search

Dark theme | Light theme

April 10, 2023

Spocklight: Testing Asynchronous Code With PollingConditions

In a previous blog post we learned how to use DataVariable and DataVariables to test asynchronous code. Spock also provides PollingConditions as a way to test asynchronous code. The PollingConditions class has the methods eventually and within that accept a closure where we can write our assertions on the results of the asynchronous code execution. Spock will try to evaluate conditions in the closure until they are true. By default the eventually method will retry for 1 second with a delay of 0.1 second between each retry. We can change this by setting the properties timeout, delay, initialDelay and factor of the PollingConditions class. For example to define the maximum retry period of 5 seconds and change the delay between retries to 0.5 seconds we create the following instance: new PollingConditions(timeout: 5, initialDelay: 0.5).
Instead of changing the PollingConditions properties for extending the timeout we can also use the method within and specify the timeout in seconds as the first argument. If the conditions can be evaluated correctly before the timeout has expired then the feature method of our specification will also finish earlier. The timeout is only the maximum time we want our feature method to run.

In the following example Java class we have the methods findTemperature and findTemperatures that will try to get the temperature for a given city on a new thread. The method getTemperature will return the result. The result can be null as long as the call to the WeatherService is not yet finished.

package mrhaki;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;

class AsyncWeather {

    private final ExecutorService executorService;
    private final WeatherService weatherService;

    private final Map<String, Integer> results = new ConcurrentHashMap<>();

    AsyncWeather(ExecutorService executorService, WeatherService weatherService) {
        this.executorService = executorService;
        this.weatherService = weatherService;
    }

    // Invoke the WeatherService in a new thread and store result in results.
    void findTemperature(String city) {
        executorService.submit(() -> results.put(city, weatherService.getTemperature(city)));
    }

    // Invoke the WeatherService in a new thread for each city and store result in results.
    void findTemperatures(String... cities) {
        Arrays.stream(cities)
              .parallel()
              .forEach(this::findTemperature);
    }

    // Get the temperature. Value can be null when the WeatherService call is not finished yet.
    int getTemperature(String city) {
        return results.get(city);
    }

    interface WeatherService {
        int getTemperature(String city);
    }
}

To test the class we write the following specification using PollingConditions:

package mrhaki

import spock.lang.Specification
import spock.lang.Subject
import spock.util.concurrent.PollingConditions

import java.util.concurrent.Executors

class AsyncPollingSpec extends Specification {

    // Provide a stub for the WeatherService interface.
    // Return 21 when city is Tilburg and 18 for other cities.
    private AsyncWeather.WeatherService weatherService = Stub() {
        getTemperature(_ as String) >> { args ->
            if ("Tilburg" == args[0]) 21 else 18
        }
    }

    // We want to test the class AsyncWeather
    @Subject
    private AsyncWeather async = new AsyncWeather(Executors.newFixedThreadPool(2), weatherService)

    void "findTemperature and getTemperature should return expected temperature"() {
        when:
        // We invoke the async method.
        async.findTemperature("Tilburg")

        then:
        // Now we wait until the results are set.
        // By default we wait for at  most 1 second,
        // but we can configure some extra properties like
        // timeout, delay, initial delay and factor to increase delays.
        // E.g. new PollingConditions(timeout: 5, initialDelay: 0.5, delay: 0.5)
        new PollingConditions().eventually {
            // Although we are in a then block, we must
            // use the assert keyword in our eventually
            // Closure code.
            assert async.getTemperature("Tilburg") == 21
        }
    }

    void "findTemperatures and getTemperature shoud return expected temperatures"() {
        when:
        // We invoke the async method.
        async.findTemperatures("Tilburg", "Amsterdam")

        then:
        // Instead of using eventually we can use within
        // with a given timeout we want the conditions to
        // be available for assertions.
        new PollingConditions().within(3) {
            // Although we are in a then block, we must
            // use the assert keyword in our within
            // Closure code.
            assert async.getTemperature("Amsterdam") == 18
            assert async.getTemperature("Tilburg") == 21
        }
    }
}

Written with Spock 2.4-groovy-4.0.