shimforge

Mock any Rust function. Change none of your code.

Replace std, third-party and your own functions while a test runs.

cargo add --dev shimforge
ExamplesGitHub
Production code
use std::fs;

pub fn claim_slot() -> Result<(), String> {
    fs::create_dir_all("/var/run/dispatcher")
        .map_err(|error| format!("cannot claim a slot: {error}"))
}
Test code
use shimforge::{Session, mock};
use std::io;

#[test]
fn a_denied_slot_is_reported() {
    let mut session = Session::new();
    let mkdir = mock!(session, fs::create_dir_all::<&str>, fn(&str) -> io::Result<()>);
    mkdir
        .expect()
        .with(|path| *path == "/var/run/dispatcher")
        .once()
        .returning(|_| Err(io::ErrorKind::PermissionDenied.into()));

    let error = claim_slot().unwrap_err();
    assert!(error.starts_with("cannot claim a slot"));
}

Why shimforge

Common reasons Rust code is hard to unit test.

No test-only traits

Trait-based mocking makes you rewrite production code so a test can pass in a fake. With shimforge, the code stays as it is.

rewritten for trait-based mocking
use std::{fs, io};

pub trait FileSystem {
    fn create_dir_all(&self, path: &str) -> io::Result<()>;
}

pub struct RealFileSystem;

impl FileSystem for RealFileSystem {
    fn create_dir_all(&self, path: &str) -> io::Result<()> {
        fs::create_dir_all(path)
    }
}

pub fn claim_slot(files: &impl FileSystem) -> Result<(), String> {
    files
        .create_dir_all("/var/run/dispatcher")
        .map_err(|error| format!("cannot claim a slot: {error}"))
}

// ...and every caller of claim_slot now passes &RealFileSystem.
unchanged, with shimforge
use std::fs;

pub fn claim_slot() -> Result<(), String> {
    fs::create_dir_all("/var/run/dispatcher")
        .map_err(|error| format!("cannot claim a slot: {error}"))
}

Examples

Production code, and the test that mocks its dependency.

Production code
use std::{fs, io, path::Path};

#[derive(Debug)]
pub enum ExportError {
    DiskFull,
    Io(io::Error),
}

pub fn save_report(path: &Path, csv: &str) -> Result<(), ExportError> {
    fs::write(path, csv).map_err(|error| match error.kind() {
        io::ErrorKind::StorageFull => ExportError::DiskFull,
        _ => ExportError::Io(error),
    })
}
Test code
use shimforge::{Session, mock};

#[test]
fn a_full_disk_is_reported() {
    let mut session = Session::new();
    let write = mock!(session, fs::write::<&Path, &str>, fn(&Path, &str) -> io::Result<()>);
    write
        .expect()
        .with(|path, _| *path == Path::new("reports/q3.csv"))
        .once()
        .returning(|_, _| Err(io::ErrorKind::StorageFull.into()));

    let result = save_report(Path::new("reports/q3.csv"), "region,total\n");
    assert!(matches!(result, Err(ExportError::DiskFull)));
}

Features

Match arguments, count calls, check order and control return values.

Setup

Add shimforge to Cargo.toml, then run your tests.

  1. 1

    Add the dependency

    [dev-dependencies]
    shimforge = "0.1"
  2. 2

    Run the tests

    cargo test