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.