It’s been a while since I’ve worked on a server-side application that had asynchronous behaviour that wasn’t already an event-driven system. Asynchronous behaviour is always an interesting challenge to design and test. In general, asynchronous behaviour should not be hard to unit test – after all, the behaviour of an action shouldn’t necessarily be coupled temporally (see forms of coupling).
TIP: If you are finding the need for async testing in your unit tests, you’re probably doing something wrong and need to redesign your code to decouple these concerns.
If your testing strategy only includes unit testing, you will miss a whole bunch of behaviour which are often caught at high level of testing like integration, functional or system tests – which is where I need asynchronous testing.
Asychronous testing, conceptually, is actually pretty easy. Like synchronous testing, you take an action and then look for a desired result. However unlike synchronous testing, your test cannot guarantee that the action has completed before you check for the side-effect or result.
There are generally two approaches to testing asynchronous behaviour:
- Remove the asynchronous behaviour
- Poll until you have the desired state
Remove the asynchronous behaviour
I used this approach when TDD-ing a thick client application many years ago, when writing applications in swing applications was still a common approach. Doing this required isolating the action invoking behaviour into a single place, that, instead of it occurring in a different thread would, during the testing process, occur in the same thread as the test. I even gave a presentation on it in 2006, and wrote this cheatsheet talking about the process.
This approach required a disciplined approach to design where toggling this behaviour was isolated in a single place.
Poll until you have the desired state
Polling is a much more common approach to this problem however this involves the common problem of waiting and timeouts. Waiting too long increases your overall test time and extends the feedback loop. Waiting too short might also be quite costly depending on the operation you have (e.g. hammering some integration point unnecessarily).
Timeouts are another curse of asynchronous behaviour because you don’t really know when an action is going to take place, but you don’t really want a test going forever.
The last time I had to do something, we would often end up writing our own polling and timeout hook, while relatively simple is now available as a very simple library. Fortunately other people have also encountered this problem in java-land and contributed a library to help make testing this easier in the form of Awaitility.
Here is a simple test that demonstrates how easy the library can make testing asynchronous behaviour:
package com.thekua.spikes.aysnc.testing; import com.thekua.spikes.aysnc.testing.FileGenerator; import org.junit.Before; import org.junit.Test; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; public class FileGeneratorTest { private static final String RESULT_FILE = "target/test/resultFile.txt"; private static final String STEP_1_LOG = "target/test/step1.log"; private static final String STEP_2_LOG = "target/test/step2.log"; private static final String STEP_3_LOG = "target/test/step3.log"; private static final List<String> FILES_TO_CLEAN_UP = Arrays.asList(STEP_1_LOG, STEP_2_LOG, STEP_3_LOG, RESULT_FILE); @Before public void setUp() { for (String fileToCleanUp : FILES_TO_CLEAN_UP) { File file = new File(fileToCleanUp); if (file.exists()) { file.delete(); } } } @Test public void shouldWaitForAFileToBeCreated() throws Exception { // Given I have an aysnc process to run String expectedFile = RESULT_FILE; List<FileGenerator> fileGenerators = Arrays.asList( new FileGenerator(STEP_1_LOG, 1, "Step 1 is complete"), new FileGenerator(STEP_2_LOG, 3, "Step 2 is complete"), new FileGenerator(STEP_3_LOG, 4, "Step 3 is complete"), new FileGenerator(expectedFile, 7, "Process is now complete") ); // when it is busy doing its work ExecutorService executorService = Executors.newFixedThreadPool(10); for (final FileGenerator fileGenerator : fileGenerators) { executorService.execute(new Runnable() { public void run() { fileGenerator.generate(); } }); } // then I get some log outputs await().atMost(2, SECONDS).until(testFileFound(STEP_1_LOG)); await().until(testFileFound(STEP_2_LOG)); await().until(testFileFound(STEP_3_LOG)); // and I should have my final result with the output I expect await().atMost(10, SECONDS).until(testFileFound(expectedFile)); String fileContents = readFile(expectedFile); assertThat(fileContents, startsWith("Process")); // Cleanup executorService.shutdown(); } private String readFile(String expectedFile) throws IOException { return new String(Files.readAllBytes(Paths.get(expectedFile))); } private Callable<Boolean> testFileFound(final String file) { return new Callable<Boolean>() { public Boolean call() throws Exception { return new File(file).exists(); } }; } }
You can explore the full demo code on this public git repository.
Recent Comments