The Tests That Earn Their Keep
Build a Chess Game with Rust and WebAssembly
Fourteen tests already guard the happy paths, because each lesson shipped
its own. This lesson writes the other kind: the fixtures that live where
chess actually breaks software — state that doesn’t round-trip, endings
that aren’t checkmate, positions that lie. No production code changes at
all; by the end, the Rust suite reads as a specification. Checkpoint:
lesson-10.
Round trips are where saves go to die
Lesson 9’s persistence rests entirely on one claim: get_fen →
try_set_fen loses nothing. Three tests attack it. The first saves after
- e4 and asserts the string itself carries the turn and castling rights before restoring:
let saved = g.get_fen();
assert!(saved.contains(" b "), "turn must be saved: {saved}");
assert!(saved.contains("KQkq"), "castling rights must be saved: {saved}");
The second proves castling is still playable after a round trip — not
just that the rights field looks right, but that O-O actually executes
in the restored game. The third is the one that catches real bugs in real
chess apps: set up a position where en passant is legal, round-trip it,
and capture en passant in the restored game. If get_fen used
EnPassantMode::Always or the restore path dropped the field, this test
is the only thing that would tell you. A save format tested only by
“restore then compare” can still lose rights the comparison doesn’t
exercise; these assert on behavior.
Endings that aren’t checkmate
Fool’s mate — f3 e5 g4 Qh4 — gives the suite a real checkmate in four
plies: game over, check flagged, 0-1, and both the opponent and
try_make_move refusing to continue. Its counterpart is the ending
beginners’ engines get wrong:
g.try_set_fen("k7/2Q5/1K6/8/8/8/8/8 b - - 0 1").unwrap();
assert!(g.is_game_over());
assert!(!g.is_check());
assert_eq!(g.get_result(), "1/2-1/2");
Black has no legal move and is not in check: stalemate, a draw, half the
point gone. The !is_check() assert is the line doing the work —
it’s what distinguishes this ending from mate for any code that consumes
get_result.
The last fixture feeds try_set_fen a board with two white kings.
Syntactically valid FEN, impossible chess — and it’s rejected by
shakmaty’s into_position stage, not the string parser. That’s the
lesson-3 two-stage design proving it was two stages for a reason.
What nineteen tests cost and buy
Run the suite and watch the whole thing finish in a few milliseconds —
cargo test compiles to native code via the rlib target, so there’s no
wasm, no browser, no fixture server anywhere. That speed is a design
outcome, and it’s worth stating the causal chain plainly: the boundary
keeps JS types out of the core, the core stays natively compilable, so the
tests cost nothing to run and you run them constantly.
If you want a number for coverage, cargo llvm-cov reports on this crate
nicely (install cargo-llvm-cov first; the repo wires it as
coverage:rust in a later lesson). But the more useful audit is reading
the test names top to bottom and checking they describe the rules of the
crate rather than its implementation — that’s what makes a suite double
as documentation.
Build it
| File | Action | What goes in it |
|---|---|---|
chess-engine/src/lib.rs |
modify | Five new tests (full_fen_round_trip_preserves_state, castling_rights_survive_round_trip, en_passant_survives_round_trip, cpu_has_no_moves_after_checkmate, stalemate_is_a_draw) plus the two-kings assert added to rejects_invalid_fen |
Done when: npm run test:rust reports 19 passed.
Answer key: lesson-09...lesson-10.
Challenge: the draw the engine can’t see
Threefold repetition and the fifty-move rule are draws too. Write a test
shuffling knights back and forth and discover what is_game_over says —
then work out why from shakmaty’s docs: a Chess position doesn’t carry
its own history, so repetition is unknowable from inside it. Decide where
that responsibility would have to live in this architecture, and write
your finding as a comment on the test.
Next lesson the tests cross the boundary: the actual component, running the actual wasm, in a DOM that isn’t a browser.