facts: Porting Midje's Arrow to Rust

Stock Rust testing is fine, in the sense that a hammer is fine. You get assert!, assert_eq!, assert_ne!, and the #[test] attribute. Everything past that — better diffs, fuzzy float comparisons, collection matchers, mocks, table-driven cases — is a separate crate with its own conventions: pretty_assertions, assert2, claim, approx, mockall, rstest, proptest. Each crate is good. Together they form a Galápagos of small DSLs that all read slightly differently.

The most common micro-friction is the reading order of an equality assertion:

assert_eq!(2 + 2, 4);

Which side is the expected value? The convention says left, but the compiler doesn’t enforce it, the failure message labels them left and right, and you can search any large Rust codebase and find both orderings within the same file. The information is there. The reading order is not.

I’ve been writing Rust for a while, but I keep noticing the same friction I noticed years ago in JUnit and Python’s unittest: the assertion vocabulary is a flat namespace of assert_* verbs, and there’s no consistent place to put the thing being tested versus the expectation about it. You learn to navigate it, the way you learn to navigate a dialect. But every now and then a test file from a different tradition shows up and you remember it doesn’t have to be like this.

For me the most striking counter-example has always been Midje, Brian Marick’s testing library for Clojure. So I’ve been building facts, a Rust library that ports the core of Midje’s reading model into Rust without giving up cargo test. This post is about what survives the port, what doesn’t, and why.

The whole of Midje builds on a single arrow:

(fact "arithmetic"
  (+ 2 2) => 4
  (Math/sqrt 2) => (roughly 1.414 0.001)
  (parse "hi") => truthy)

Three things to notice.

First, the arrow => defines reading order. Left is what the code does. Right is what we expect. There is exactly one place to put each. You read every line of every test the same way, in the same direction, forever.

Second, the right-hand side is overloaded but consistently so. 4 is a value. (roughly 1.414 0.001) is a checker — a function that takes the actual value and decides if it matches. truthy is a checker too. The arrow is the same; what varies is the richness of the expectation. You don’t reach for a new macro every time you want fuzzy or partial matching.

Third, mocking lives in the same notation:

(fact "greets in the morning"
  (greet) => "Good morning"
  (provided
    (clock/now) => 8
    (clock/tzname) => "UTC"))

provided redefines functions for the duration of one fact. The same => is used to stub the dependency. Reading order and shape don’t change.

These three properties — one arrow, one reading order, one notation — are what I wanted in Rust. The rest of Midje (arrow families, tabular facts, backgrounds, autotest) is built on top of that foundation.

The shipped surface looks like this:

use facts::prelude::*;

#[test]
fn arithmetic() {
    fact!(2 + 2 => 4);
    fact!((2.0_f64).sqrt() => roughly(1.414, 0.001));
    fact!(vec![1, 2, 3] => contains(2));
}

fact! is a declarative macro. It captures both sides of the =>, runs the right-hand side as a checker against the left-hand value, and panics on failure with a message that includes the source text, the actual value, and the checker’s description. Every fact! is a plain assertion living inside an ordinary #[test] function, which is the only non-negotiable: cargo test, --nocapture, name filters, and test parallelism all keep working. There is no global runner state.

refute! is the negation:

refute!("not equal", 2 => 3);

The optional leading string label surfaces in the failure output. The same shape exists in fact!.

There are grouped facts, where the macro generates the #[test] fn for you:

facts! { strings, "prefix and suffix checks"
    fact!("Dr. Strange" => has_prefix("Dr."));
    fact!("Dr. Strange" => has_suffix("Strange"));
}

And tables:

tabular! {
    fact!(a + b => sum),
    (a, b, sum),
    (1, 2, 3),
    (4, 5, 9),
    (-1, 1, 0),
}

Each row expands into a let-binding plus a fact!. The columns are type-checked by rustc because the generated let is an ordinary destructure.

The right-hand side is the interesting part. Let me show why.

The arrow is held up by one trait:

pub trait Checker<T: ?Sized> {
    fn check(&self, actual: &T) -> Outcome;
}

impl<T: PartialEq + Debug> Checker<T> for T {
    /* eq check */
}

Custom checkers — Roughly, Contains<T>, HasPrefix, IsOk, Truthy — are distinct types that each implement Checker<Target> for some target. The blanket implementation says that any T that is PartialEq + Debug is also a Checker<T>, doing an equality check. Because Roughly is not a number and Contains<T> is not a Vec, there is no specialization conflict — the type system sees them as separate impls.

That one trick is what makes fact!(2 + 2 => 4) and fact!(x => roughly(1.414, 0.001)) go through the same code path. The arrow does not care which one you wrote. Reading order stays constant; the vocabulary on the right grows as you add checker types.

Writing your own checker is mechanical:

use facts::{Checker, Outcome};

pub struct Even;
pub fn even() -> Even { Even }

impl Checker<i32> for Even {
    fn check(&self, actual: &i32) -> Outcome {
        if actual % 2 == 0 {
            Outcome::pass()
        } else {
            Outcome::fail(format!("{} is not even", actual))
        }
    }
}

// fact!(4 => even());

That’s the whole extension story. No traits to register, no derives, no plugin macros — a struct, a constructor, and a single method.

Some of Midje’s design does not translate cleanly. The interesting parts of this project are the places where Rust’s grammar pushed back.

Negation is a separate macro, not an arrow. I wanted !=> for refutation, to keep the arrow family feel. But declarative macros capture the left-hand side with :expr, and ! is a valid prefix operator in Rust expressions. The parser can’t reliably tell (!a) => from a !=>. So refute! is its own macro. The cost is one extra identifier; the benefit is that no fact ever has its meaning silently flipped by a stray !.

Tables use tuples, not pipes. Midje’s tabular form looks like | a | b | sum | with pipe-delimited rows. The first sketch tried to mirror that, but | opens closures in Rust expressions, and the :expr capture chokes on it. So rows are parenthesized tuples instead. The shape is a little more LISP, a little less Markdown, but the destructure-then-evaluate semantics survive intact.

Arrow family overload is gone. Midje has =future=>, =streams=>, =throws=>, and a few others. Each is a different arrow for a different kind of check. In Clojure that’s charming. In Rust, where every macro arm has to disambiguate against the others, it would be noise. facts has exactly one arrow plus refute!. Asynchronous tests just use .await on the left-hand side; throwing tests use checkers like is_err().

No autotest. Midje ships its own file watcher. cargo-watch already exists; it’s the same idea and it works for any Rust project. Re-implementing it inside a test library would only fragment the ecosystem.

No global state. Midje keeps some context in dynamic vars (midje.sweet/*report* and friends). facts keeps every fact! as a plain assert. Nothing leaks between tests. The cost is that some Midje features (a global “background” that wraps every fact in a namespace) become per-facts!-block instead of file-global.

These are all places where I deliberately undershot Midje. The library is smaller than its inspiration, on purpose. The arrow is what mattered.

Clojure has with-redefs, which rebinds vars for the duration of a form. Midje’s provided uses it to stub any function in your codebase. Rust has nothing equivalent — function items are not rebindable at runtime — and trying to fake one with unsafe symbol patching would be wildly out of scope for a test library.

So facts does the idiomatic Rust thing instead: dependencies are trait objects, and mocks are concrete types that implement those traits.

use facts::prelude::*;

mockable! {
    pub trait Clock as ClockMock {
        fn now(&self) -> u32;
        fn tzname(&self) -> String;
    }
}

fn greet(c: &dyn Clock) -> &'static str {
    match c.now() {
        0..=11 => "Good morning",
        12..=17 => "Good afternoon",
        _ => "Good evening",
    }
}

#[test]
fn greets_in_the_morning() {
    let clock = ClockMock::new();
    provided! {
        clock.now() => 8;
        clock.tzname() => "UTC".to_string();
    }
    fact!(greet(&clock) => "Good morning");
}

mockable! generates two things: the trait you wrote, plus a sibling struct whose fields are RefCell<Option<Return>> slots — one per method. The trait impl on the mock reads the slot for the called method, clones the value out, and panics if the slot is unset. provided! is pure syntactic sugar for writing values into those slots.

Why as ClockMock instead of auto-deriving a name like MockClock? Because synthesizing identifiers in a declarative macro requires a paste-style dependency, and the explicit name is one fewer transitive crate to vendor. Why Clone return values? Because each call to the mock clones a value out of the slot, which is simpler than the alternatives (take, reference counting, full call queues) for a first pass.

The scope is deliberately narrow: &self methods, Clone returns, no argument matchers, no call-count assertions, no generics. Those are on the roadmap. The point of this first cut is to show that Midje’s provided shape survives the move to a statically-typed, ownership-tracked language as long as you commit early to trait-based seams. Which is what most idiomatic Rust does anyway.

facts is 0.0.1. The API will change. The proc-macro work to derive better spans and error messages hasn’t happened yet — the current declarative macros use stringify! and file!/line!, which is enough for failure messages to be readable but not as precise as a real proc-macro with Span plumbing. Argument matchers, call-count verification, async helpers, and a proptest bridge are all imagined but not built.

I’m publishing it early because the design reflection — what survives the port, what doesn’t, what the type system makes harder or easier than I expected — is the part I want feedback on. The arrow holds up. The blanket Checker impl is a small idea that does a lot of work. The mocking story compresses well into a trait + struct pair. Everything else is implementation.

If you’ve used Midje and you write Rust, I’d love to know what’s missing. And if you’ve never touched Midje but the arrow makes your tests easier to read, that’s the point.