Testing multi-threaded asynchronous code in Java


A test operates in one thread. The code under test runs in other threads, which your test knows nothing about.

So how do you test multi-threaded code?

I’ll show you how I did that to test my multi-client Java echo server.

Here is my server code:

// src/main/java/EchoServer.java

public class EchoServer {
  private ServerSocket serverSocket;

  EchoServer(ServerSocket serverSocket) {
    this.serverSocket = serverSocket;
    this.executor = Executors.newFixedThreadPool(2);
  }

  public void start() {
    while(true) {
      listenForClients();
    }
  }

  private void listenForClients() {
    Socket socket = serverSocket.accept();
    executor.execute(new EchoServerThread(socket));
  }
}

Initially, I set off to make all my code run in the same thread. If it’s all in the same thread, it should be easy to test, right?

Let’s use dependency injection to enable our test to replace the multi-thread executor with a single-thread one:

// src/main/java/EchoServer.java

public class EchoServer {
  private ServerSocket serverSocket;
  private Executor executor;

  EchoServer(ServerSocket serverSocket, Executor executor) {
    this.serverSocket = serverSocket;
    this.executor = executor;
  }

  public void start() {
    while(true) {
      listenForClients();
    }
  }

  private void listenForClients() {
    Socket socket = serverSocket.accept();
    executor.execute(new EchoServerThread(socket));
  }
}

In the test, let’s inject the single-thread executor:

// src/test/java/EchoServerTest.java
public class EchoServerTest {
  @Test
  public void broadcastsToAllClients() {
    // set up fake server socket
    // create three clients

    Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
    EchoServer echoServer = new EchoServer(fakeServerSocket, singleThreadExecutor));

    echoServer.listenForClients();
    echoServer.listenForClients();
    echoServer.listenForClients();

    String expected = "hello from client 1\nhello from client 2\nhello from client 3";

    assertEquals(expected, client1.getSocket().getOutputStream().toString().trim());
  }

Let’s run the test …

  // System OUT: Accepting client ...
  // System OUT: Accepted client.

  // System OUT: Accepting client ...
  // System OUT: Accepted client.

  // System OUT: Accepting client ...
  // System OUT: Accepted client.

    org.junit.ComparisonFailure

    [stacktrace]

  // System OUT: Accepted client on thread: pool-2-thread-1

    Process finished with exit code 255

… and fail.

Do you notice anything weird?

The last call to System.out occured after the test assertion failed.

The test ran in the main thread, the code in another. The test completely ignored it and completed before it had a chance to finish execution.

Wait a second

Let’s make the test wait for the thread to finish running:

// src/test/java/EchoServerTest.java

public class EchoServerTest {
  @Test
  public void broadcastsToAllClients() {
    // set up fake server socket
    // create three clients

    Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
    EchoServer echoServer = new EchoServer(fakeServerSocket, singleThreadExecutor));

    echoServer.listenForClients();
    echoServer.listenForClients();
    echoServer.listenForClients();

    Thread.sleep(1000);

    String expected = "hello from client 1\nhello from client 2\nhello from client 3";

    assertEquals(expected, client1.getSocket().getOutputStream().toString().trim());
  }
  Accepting ...
  Accepted.

  Accepting ...
  Accepted.

  Accepting ...
  Accepted.

  Accepted client on thread: pool-2-thread-1
  Number of clients connected: 1

  Accepted client on thread: pool-2-thread-1
  Number of clients connected: 2

  Accepted client on thread: pool-2-thread-1
  Number of clients connected: 3

  Tests passed: 1 of 1 test

Ok so our test passed, but making our test wait for an arbitrary 1000ms (Thread.sleep(1000)) is an antipattern. We don’t want to make our test suite slower. Quite the opposite.

Asynchronous code, synchronous test

One key learning tidbit here for me was that even if you get your test to run your code in a single thread rather than multiple ones, your test and your code still run in two different threads that are totally oblivious to each other.

The solution here is to get rid off all asynchronicity and run everything in the main thread.

We can replace

Executor executor = Executors.newSingleThreadExecutor();

with

Executor executor = new SynchronousExecutor();

where SynchronousExecutor overrides the Executor class’ execute method to run any Runnable object synchronously:

public class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable runnable) {
      runnable.run();
  }
}

The test still passes!

Key takeaway of the week: Make asynchronous code run synchronously in your test.