---
title: Porting Midje's Arrow to Rust (facts)
url: https://www.grahambrooks.com/post/facts-rust-testing-library/
date: 2026-05-25T10:00:00-06:00
author: Graham Brooks
description: Why Clojure's `actual => expected` reads better than the testing toolbox Rust ships with, and what survives the port.
tags:
- rust
- Testing
- developer-tools
---

# Porting Midje's Arrow to Rust (facts)

## The reading order problem

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:

```rust
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](https://github.com/marick/midje), Brian Marick's testing library for Clojure. So I've been building [`facts`](https://github.com/grahambrooks/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.

## What Midje gets right

The whole of Midje builds on a single arrow:

```clojure
(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:

```clojure
(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.

## What `facts` does

The shipped surface looks like this:

```rust
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:

```rust
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:

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

And tables:

```rust
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 `Checker` blanket impl

The arrow is held up by one trait:

```rust
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:

```rust
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.

## Tradeoffs the port had to make

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.

## Mocks without `with-redefs`

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.

```rust
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.

## What this is not

`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.

- Repo: <https://github.com/grahambrooks/facts>
- Midje: <https://github.com/marick/midje>
