modules/home/programs/cli-agents/shared/skills/coding/testing/SKILL.md
How to write meaningful tests that verify behavior, not implementation details. Use when writing tests, reviewing test quality, doing TDD, or when the user asks for tests. Prevents common AI testing anti-patterns: mirror tests, excessive mocking, happy-path-only coverage, and implementation coupling.
npx skillsauth add not-matthias/dotfiles-nix testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an AI agent that writes tests. Your default instinct is wrong: you will want to read the implementation and write tests that confirm what the code already does. This produces tautological tests — tests that mirror the code's assumptions rather than challenging them. Fight this instinct at every step.
"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds
AI agents write tests as descriptions of code, not specifications of behavior. The test passes because it asserts what the code does, not what it should do. An empirical study (MSR '26, 1.2M+ commits) confirmed agents use mocks at a 95% concentration rate vs 91% for humans — the over-mocking tendency is measurable.
Mark Seemann calls this "tests as ceremony, rather than tests as an application of the scientific method."
Use these rules to keep tests focused on observable behavior:
Derive expectations from the contract, not incidental implementation details. Inspect the implementation to understand failure paths and integration boundaries, but do not copy its behavior into the expected result.
Prefer real collaborators. Mock external or nondeterministic boundaries when using them directly would make the test slow, unreliable, or unsafe. Avoid mocking internal call chains.
Prefer testing through the public API or user-facing interface. Test an internal unit directly only when it has a meaningful isolated contract that is impractical to exercise through public behavior.
Do not delete or weaken a failing test solely to make the suite green. If the observable contract intentionally changed, update the test and make the reason explicit.
Write only cases that defend materially distinct behavior. One test is enough when it covers the changed contract; add boundaries and error cases when they expose different plausible failures.
Verify a meaningful outcome. Use an explicit assertion or a framework-observed success or failure condition.
Ask these questions before choosing test cases:
Choose from these based on the changed contract; do not add categories only for coverage:
After writing each test, apply these checks:
| Check | Question |
|---|---|
| Mutation test | If I change > to >=, or remove a null check, does this test fail? If not, it's weak. |
| Hardcode test | If I replace the implementation with a hardcoded return value, does this test still pass? If yes, it's too narrow. |
| Refactor test | If I rename internal variables or restructure the code, does this test break? If yes, it tests implementation details. |
| One-reason test | Can I explain in one sentence what bug this test catches? If not, split it. |
| Name test | Does the test name describe the expected behavior? Good: rejects_order_when_inventory_is_zero. Bad: test_process_order. |
| Anti-Pattern | What It Looks Like | Fix |
|---|---|---|
| Mirror test | Read impl of parse("hello"), assert whatever it returns | Decide what parse("hello") should return from the spec, then assert |
| Mock fest | Mock the DB, logger, config, clock, and half the module | Use real test DB or in-memory equivalent; only mock true external boundaries |
| Implementation coupling | expect(spy).toHaveBeenCalledWith('_internalMethod') | Assert on observable output or side effects instead |
| Happy path only | Only test add(2, 3) == 5 | Add only materially distinct boundaries or errors the contract requires |
| Kitchen sink | One test with 15 assertions checking everything | One behavior per test, one clear reason to fail |
| Snapshot addiction | toMatchSnapshot() on everything | Assert specific values; snapshots hide what matters |
| Test the framework | Assert that useState updates state | Test your code's behavior, not library internals |
Test-first is useful when the contract is clear because expectations are established before implementation details can influence them.
When fixing a bug, add the smallest regression test when it protects observable behavior and would have failed before the fix. If existing tests already cover it or a durable test is impractical, reproduce the failure and verify the narrowest relevant path instead.
When writing tests after implementation, inspect the code to identify boundaries and hidden failure paths, but derive expected values from the observable contract.
For complex pure functions (parsers, validators, serializers, math), consider property-based tests instead of (or alongside) example-based tests. They catch edge cases you wouldn't think to write by generating hundreds of random inputs.
When to use:
parse(serialize(x)) == xWhen NOT to use:
| Situation | Test Type | |---|---| | Pure function with complex logic | Unit tests (many inputs) + property-based if fitting | | API endpoint / route handler | Integration test with real-ish DB | | UI component | Integration test (render + interact + assert DOM) | | Critical user workflow | E2E test (signup, checkout, etc.) | | Bug fix | Regression test at lowest level that reproduces it | | Glue code / simple delegation | Usually don't test — integration tests cover it |
Cover use cases, not lines.
No numeric coverage target. Instead, for every public function/endpoint/component, require:
If a function is too trivial to warrant three tests (getter, simple delegation), it probably doesn't need a dedicated test — integration tests will cover it.
rstest for parameterized tests — collapse boundary cases into one testproptest/quickcheck for property-based testing on complex pure functionsResult::Err variants explicitly — don't just test the Ok path@pytest.mark.parametrize to collapse boundary caseshypothesis for property-based testing on complex pure functionspytest.raises(ExceptionType, match="...") for error pathsuserEvent over fireEventmsw for API mocking instead of mocking fetch directlytoMatchSnapshot is almost never the right choiceThe implementation is hidden — you only see the contract. This is how you should approach testing.
pub struct Cart { /* hidden */ }
impl Cart {
pub fn new() -> Self;
/// Quantity must be 1..=99
pub fn add(&mut self, item_id: &str, price: f64, qty: u32) -> Result<()>;
/// Errors if item not in cart
pub fn remove(&mut self, item_id: &str) -> Result<()>;
/// Always >= 0.0, includes discounts
pub fn total(&self) -> f64;
/// Single-use. Errors if invalid or already used.
pub fn apply_discount(&mut self, code: &str) -> Result<()>;
}
#[test]
fn test_add() {
let mut cart = Cart::new();
cart.add("hat", 25.0, 1).unwrap();
assert_eq!(cart.items.len(), 1); // internal field!
assert_eq!(cart.items[0].qty, 1); // internal field!
}
#[test]
fn test_total() {
let mut cart = Cart::new();
cart.add("hat", 25.0, 2).unwrap();
assert_eq!(cart.total(), 50.0); // just mirrors 25*2
}
test_add reaches into cart.items — breaks if storage changes from Vec to HashMap. test_total only checks the obvious multiplication, derived from reading the impl. Neither test catches real bugs.
fn cart_with(item_id: &str, price: f64, qty: u32) -> Cart {
let mut c = Cart::new();
c.add(item_id, price, qty).unwrap();
c
}
#[rstest]
#[case(0, true)] // below range
#[case(1, false)] // lower bound
#[case(99, false)] // upper bound
#[case(100, true)] // above range
fn add_rejects_invalid_quantity(
#[case] qty: u32,
#[case] should_err: bool,
) {
let result = Cart::new().add("hat", 10.0, qty);
assert_eq!(result.is_err(), should_err);
}
#[test]
fn remove_nonexistent_item_errors() {
assert!(Cart::new().remove("nope").is_err());
}
#[test]
fn discount_code_is_single_use() {
let mut cart = cart_with("hat", 100.0, 1);
cart.apply_discount("SAVE10").unwrap();
assert!(cart.apply_discount("SAVE10").is_err());
}
#[test]
fn total_never_negative_after_large_discount() {
let mut cart = cart_with("hat", 5.0, 1);
let _ = cart.apply_discount("HALF_OFF");
assert!(cart.total() >= 0.0);
}
4 tests. Each targets a non-obvious spec constraint: quantity boundaries (parameterized), missing-item error, single-use invariant, non-negative invariant. The happy path is covered implicitly by cart_with in the other tests — no dedicated test needed.
Parameterized tests shine when testing a single function against many inputs. Use two test functions (positive and negative) instead of a boolean parameter — it's more readable and the test names are self-documenting.
#[test]
fn basic_java_command() {
assert!(command_has_executable("java -jar bench.jar", &["java"]));
}
#[test]
fn java_with_absolute_path() {
assert!(command_has_executable("/usr/bin/java -jar bench.jar", &["java"]));
}
#[test]
fn java_with_env_prefix() {
assert!(command_has_executable("FOO=bar java -jar bench.jar", &["java"]));
}
#[test]
fn gradle_chained_with_and() {
assert!(command_has_executable("cd /app && gradle bench", &["gradle"]));
}
#[test]
fn javascript_must_not_match_java() {
assert!(!command_has_executable("javascript-runtime run", &["java"]));
}
#[test]
fn javascript_path_must_not_match_java() {
assert!(!command_has_executable("/home/user/javascript/run.sh", &["java"]));
}
#[test]
fn scargoship_must_not_match_cargo() {
assert!(!command_has_executable("scargoship build", &["cargo"]));
}
7 tests, lots of boilerplate. Adding a new case means copy-pasting an entire function. The positive/negative intent is buried in assert! vs assert!.
use rstest::rstest;
#[rstest]
#[case("java -jar bench.jar", &["java"])]
#[case("/usr/bin/java -jar bench.jar", &["java"])]
#[case("FOO=bar java -jar bench.jar", &["java"])]
#[case("cd /app && gradle bench", &["gradle"])]
#[case("cat file | python script.py", &["python"])]
#[case("sudo java -jar bench.jar", &["java"])]
#[case("(cd /app && java -jar bench.jar)", &["java"])]
#[case("setup.sh; java -jar bench.jar", &["java"])]
#[case("try_first || java -jar bench.jar", &["java"])]
fn matches(#[case] command: &str, #[case] names: &[&str]) {
assert!(command_has_executable(command, names));
}
#[rstest]
#[case("javascript-runtime run", &["java"])]
#[case("/home/user/javascript/run.sh", &["java"])]
#[case("scargoship build", &["cargo"])]
#[case("node index.js", &["gradle", "java", "maven", "mvn"])]
fn does_not_match(#[case] command: &str, #[case] names: &[&str]) {
assert!(!command_has_executable(command, names));
}
Same coverage, adding a case is one line. The function names (matches / does_not_match) make the intent obvious without reading the assertion.
documentation
Save notes, journal entries, and research to the personal-notes Obsidian vault (personal-vault-v2). Use when the user asks to 'save note', 'save to notes', 'write to personal notes', 'save to daily notes', 'note this down', or wants to persist findings/analysis to their personal vault.
documentation
Use whenever the user asks to address, fix, resolve, review, or respond to pull-request comments or review feedback.
development
Apply Not Matthias's Rust-first personal coding style. Use whenever the user explicitly asks to apply or review their code style, make Rust match their preferences, perform a style pass, or simplify/refactor according to their conventions. Inspect only task-touched code, honor local project conventions first, and make only safe opt-out style edits.
development
Guide for writing ast-grep rules to perform structural code search and analysis. Use when users need to search codebases using Abstract Syntax Tree (AST) patterns, find specific code structures, or perform complex code queries that go beyond simple text search. This skill should be used when users ask to search for code patterns, find specific language constructs, or locate code with particular structural characteristics.