Testing a Rust binary crate

3 minute read Published:

Using std::process to write Rust tests for a binary crate, i.e. a crate that doesn't expose clean public functions like a library crate.

I recently wrote a binary crate in Rust called pq. In a nutshell, pq generically decodes protobuf messages into json given a collection of *.fdset files. This is useful in a scenario where you maintain several protobuf message schemas, and you need a quick way to eyeball a message encoded in any one of those types without knowing the exact type.

Here’s the source code on GitHub.

Since it’s a binary crate without an exposed lib.rs I researched idiomatic ways to run Rust tests on it. I discovered that xsv runs tests by invoking the binary with std::process.

Binary path

First, the Rust test suite needs to know how to find the binary. This piece of code was inspired by xsv: the runner module.

It gets the directory of the current test executable (which will also contain the compiled binary, pq):

let mut root = env::current_exe()
    .expect("executable's directory")

Tests path

In my case I also have some sample encoded protobuf message files in tests which I need to pass to my binary to check the result, so I find the tests path from the executable path:

let mut tests_path = root.parent().unwrap().parent().unwrap().to_path_buf();


Finally I create an std::process::Command with:

let mut cmd = Command::new(root.join("pq"));

Passing stdin

A key feature of pq is that it operates on stdin by default, as intuitive Unix tools should. I needed a way to modify the stdin of a running process. Using std::process::Child does the trick:

pub fn with_stdin(&mut self, contents: &[u8]) {
    let mut chld = self._spawn();
    self.chld = Some(chld);

Getting the output

Finally, getting an std::process::Output to return from a Child is good as it lets you check the exit code, stdout, and stderr in your unit tests:

pub fn output(&mut self) -> Output {

Invoker utility for different arguments

At this point I’ve finished describing the important functionality of the runner module. Now I move onto the actual unit test file itself:

fn for_person(work: &mut Runner) {

fn for_dog_stdin(work: &mut Runner) {
    let mut file = File::open(&work.tests_path.join("samples/dog")).unwrap();
    let mut buf = Vec::new();
    file.read_to_end(&mut buf).unwrap();

fn run_pqrs<F>(modify_in: F) -> Output
    where F: FnOnce(&mut Runner)
    let mut work = Runner::new();


    modify_in(&mut work);


Putting it together

An actual unit test with a success exit code:

fn test_person_decode() {
    let out = run_pqrs(for_person);

    //check if success

    //check output

Here’s one with an error:

fn test_nonexistent_file() {
    let out = run_pqrs(for_nonexistent_file);

    //check if success
    assert_eq!(out.status.code().unwrap(), 255);

    //check output
    assert_eq!(String::from_utf8_lossy(&out.stdout), "");

    //check stderr
               "Could not open file: file-doesnt-exist\n");