How to Mock Stdin with Cursor in Tokio

A recipe on how to inject test input using std::io::Cursor with tokio's BufReader.

I was trying to figure out how to test code that reads from stdin in tokio. I couldn’t exactly pass real stdin to my tests, and I didn’t want to use some heavy mocking library either. There had to be a simpler way.

Let me show you what I was trying to test. This is a simplified JSON-RPC server that reads from stdin and writes responses to stdout:

Rust
// Simplified from src/cmd/acp/mod.rs
use tokio::io::{BufReader, BufWriter, AsyncBufReadExt, AsyncWriteExt, stdin, stdout};

async fn run_server() -> Result<()> {
    let reader = BufReader::new(stdin());
    let mut writer = BufWriter::new(stdout());
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await? {
        if line.trim().is_empty() {
            continue;
        }

        // Parse JSON-RPC request
        let Ok(request) = serde_json::from_str::<JsonRpcRequest>(line.as_str()) else {
            let response = jsonrpc::Response::parse_error();
            let json = serde_json::to_string(&response)?;
            writer.write_all(format!("{}\n", json).as_bytes()).await?;
            continue;
        };

        if request.jsonrpc != "2.0" {
            let response = jsonrpc::Response::invalid_request(request.id);
            let json = serde_json::to_string(&response)?;
            writer.write_all(format!("{}\n", json).as_bytes()).await?;
            continue;
        }

        // Handle request...
    }

    Ok(())
}

The problem is clear. I want to write a test like this:

Rust
#[tokio::test]
async fn test_parse_error() {
    // How do I pass test input here?
    run_server().await;
}

But I can’t pass real stdin. And I can’t easily capture the output either.

It turns out you use std::io::Cursor with tokio::io::BufReader. The Cursor type implements tokio’s AsyncRead trait, so you can use it anywhere you’d use a real reader like stdin.

Here’s the basic solution:

Rust
use std::io::Cursor;
use tokio::io::{BufReader, AsyncBufReadExt};

#[tokio::test]
async fn test_parse_error() {
    // Create mock stdin
    let input = b"not valid json\n";
    let stdin_mock = Cursor::new(input);
    let reader = BufReader::new(stdin_mock);

    // Now you can use it just like you would stdin!
    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await.unwrap() {
        // Process the line...
    }
}

Let me break down what’s happening here.

First, the imports. You need std::io::Cursor from the standard library:

Rust
use std::io::Cursor;
use tokio::io::{BufReader, AsyncBufReadExt};

Then you create your test input:

Rust
let input = b"not valid json\n";  // &'static [u8]
let stdin_mock = Cursor::new(input);
let reader = BufReader::new(stdin_mock);

The key insight is that Cursor<T> implements tokio::io::AsyncRead when T: AsRef<[u8]>. Since &[u8], String, and Vec<u8> all implement AsRef<[u8]>, they all work out of the box.

One thing to notice: the input has explicit \n characters. The lines() method splits on newline, so you need to include them yourself:

Rust
let input = b"first line\nsecond line\n";  // Two lines
let input = b"no newline at end";           // One line, no trailing newline

One more tip from the tokio docs: make sure your test input ends with a newline if you’re using lines(). The last line doesn’t technically need it for the method to work, but it’s good consistency.

Other recipes in Tokio