Unit Testing

The purpose of this page is to give advice on how to write useful unit tests in asynchronous applications.

Pausing and resuming time in tests

Sometimes, asynchronous code explicitly waits by calling tokio::time::sleep or waiting on a tokio::time::Interval::tick. Testing behaviour based on time (for example, an exponential backoff) can get cumbersome when the unit test starts running very slowly. However, internally, the time-related functionality of tokio supports pausing and resuming time. Pausing time has the effect that any time-related future may become ready early. The condition for the time-related future resolving early is that there are no more other futures which may become ready. This essentially fast-forwards time when the only future being awaited is time-related:

#[tokio::test]
async fn paused_time() {
    tokio::time::pause();
    let start = std::time::Instant::now();
    tokio::time::sleep(Duration::from_millis(500)).await;
    println!("{:?}ms", start.elapsed().as_millis());
}

This code prints 0ms on a reasonable machine.

For unit tests, it is often useful to run with paused time throughout. This can be achieved simply by setting the macro argument start_paused to true:

#[tokio::test(start_paused = true)]
async fn paused_time() {
    let start = std::time::Instant::now();
    tokio::time::sleep(Duration::from_millis(500)).await;
    println!("{:?}ms", start.elapsed().as_millis());
}

See tokio::test "Configure the runtime to start with time paused" for more details.

Of course, the temporal order of future resolution is maintained, even when using different time-related futures:

#[tokio::test(start_paused = true)]
async fn interval_with_paused_time() {
    let mut interval = interval(Duration::from_millis(300));
    let _ = timeout(Duration::from_secs(1), async move {
        loop {
            interval.tick().await;
            println!("Tick!");
        }
    })
    .await;
}

This code immediately prints "Tick!" exactly 4 times.

Mocking using AsyncRead and AsyncWrite

The generic traits for reading and writing asynchronously (AsyncRead and AsyncWrite) are implemented by, for example, sockets. They can be used for mocking I/O performed by a socket.

Consider, for setup, this simple TCP server loop:

use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
    loop {
        let Ok((mut socket, _)) = listener.accept().await else {
            eprintln!("Failed to accept client");
            continue;
        };

        tokio::spawn(async move {
            let (reader, writer) = socket.split();
            // Run some client connection handler, for example:
            // handle_connection(reader, writer)
                // .await
                // .expect("Failed to handle connection");
        });
    }
}

Here, each TCP client connection is serviced by its dedicated tokio task. This task owns a reader and a writer, which are split off of a TcpStream.

Consider now the actual client handler task, especially the where-clause of the function signature:

use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};

async fn handle_connection<Reader, Writer>(
    reader: Reader,
    mut writer: Writer,
) -> std::io::Result<()>
where
    Reader: AsyncRead + Unpin,
    Writer: AsyncWrite + Unpin,
{
    let mut line = String::new();
    let mut reader = BufReader::new(reader);

    loop {
        if let Ok(bytes_read) = reader.read_line(&mut line).await {
            if bytes_read == 0 {
                break Ok(());
            }
            writer
                .write_all(format!("Thanks for your message.\r\n").as_bytes())
                .await
                .unwrap();
        }
        line.clear();
    }
}

Essentially, the given reader and writer, which implement AsyncRead and AsyncWrite, are serviced sequentially. For each received line, the handler replies with "Thanks for your message.".

To unit test the client connection handler, a tokio_test::io::Builder can be used as a mock:

#[tokio::test]
async fn client_handler_replies_politely() {
    let reader = tokio_test::io::Builder::new()
        .read(b"Hi there\r\n")
        .read(b"How are you doing?\r\n")
        .build();
    let writer = tokio_test::io::Builder::new()
        .write(b"Thanks for your message.\r\n")
        .write(b"Thanks for your message.\r\n")
        .build();
    let _ = handle_connection(reader, writer).await;
}