Monday, 2 April 2012

Testing asynchronous calls with awaitility

Testing asynchronous systems is not an easy task. Let's take a simple example:

public class FileCreator {
private ExecutorService executor = Executors.newCachedThreadPool();
public void createFile(final String fileName) {
executor.submit(new Callable<Path>() {
@Override
public Path call() throws Exception {
// simulate delay
Thread.sleep(500);
return Files.createFile(Paths.get(fileName));
}
});
}
}

We have a class that creates file based on a given file name. The interesting thing is, that it does it asynchronously using a thread pool and returns immediately to the caller.

Let's try to create a test for it. Our first attempt could look like that:

public class FileCreatorTestWithSleeping {
private FileCreator fileCreator = new FileCreator();
private String fileName = "file.txt";
@Before
public void setUp() throws IOException {
Files.deleteIfExists(Paths.get(fileName));
}
@Test
public void shouldCreateFile() throws InterruptedException {
// when
fileCreator.createFile(fileName);
Thread.sleep(1000);
// then
verifyThatFileExists(fileName);
}
private void verifyThatFileExists(String fileName) {
Path path = Paths.get(fileName);
assertThat(Files.exists(path), is(true));
}
}

The horrible thing about it is that Thread.sleep() invocation. Test should be fast, making them wait unnecessary is very poor solution. And what if the test sometimes fails because of overloaded hardware? Are we going to sleep even more?

To eliminate unneeded waiting, we may come up with a concept of validator:

public class FileCreatorTestWithCustomValidator {
private FileCreator fileCreator = new FileCreator();
private String fileName = "file.txt";
@Before
public void setUp() throws IOException {
Files.deleteIfExists(Paths.get(fileName));
}
@Test
public void shouldCreateFile() throws InterruptedException {
// when
fileCreator.createFile(fileName);
// then
boolean fileExists = checkThatFileExists(fileName);
assertThat(fileExists, is(true));
}
private boolean checkThatFileExists(String fileName)
throws InterruptedException {
for (int i = 0; i < 100; i++) {
Path path = Paths.get(fileName);
try {
assertThat(path, exists());
return true;
} catch (AssertionError e) {
// ignore exception
}
Thread.sleep(100);
}
throw new AssertionError("Timeout exceeded");
}
}

We no longer need to sleep for a long time, but the code has been significantly polluted. Of course we can refactor our validator, make it more reusable but why reinvent the wheel? There is a nice and small library - awaitility - that will do the same for us.

public class FileCreatorTestWithAwaitility {
private FileCreator fileCreator = new FileCreator();
private String fileName = "file.txt";
@Before
public void setUp() throws IOException {
Files.deleteIfExists(Paths.get(fileName));
}
@Test
public void shouldCreateFile() throws Exception {
// when
fileCreator.createFile(fileName);
// then
await().until(fileIsCreated(fileName));
}
private Callable<Boolean> fileIsCreated(final String fileName) {
return new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Path path = Paths.get(fileName);
return Files.exists(path);
}
};
}
}

In a very expressive way, we achieve the same result. Timeout, polling delay and polling interval are of course configurable.

No comments:

Post a Comment