One test, one thing — name it for the scenario, structure it AAA
Each test exercises one logical behaviour, is named after the exact scenario and expected outcome, and is laid out Arrange / Act / Assert. If the name needs _and_, it's two tests.
Why
A test name is the failure message in CI. When test_x_and_y goes red, you can't tell which behaviour broke without opening the file — and the test itself often half-passes through one branch and half-asserts the other, hiding regressions. One scenario per test means the name pins the regression, the AAA shape makes the intent scannable, and a future reader fixes the right thing instead of patching whichever assert tripped first.
How to apply
- Name = scenario + expected outcome.
<unit>_<condition>_<expected>. If you wrote_and_, split the test. - AAA visible. Pick blank-line separation OR
// arrange/// act/// assertcomments — either works, just make the three phases readable at a glance. - One thing. One logical behaviour per test. Multiple
assert!calls are fine when they all check the same behaviour from different angles (e.g. assertingresult.fooandresult.barafter one call). Multiple distinct scenarios → multiple tests.
Examples
The "and" smell (live case):
// BEFORE — one test, two scenarios, "and" in the name
#[test]
fn group_len_returns_expected_size_and_zero_for_unknown_group() { ... }
// AFTER — split into two
#[test]
fn group_len_returns_member_count_for_known_group() { ... }
#[test]
fn group_len_returns_zero_for_unknown_group() { ... }
Packed-scenarios smell (live case): created_group_flag_is_true_only_for_first_order_in_a_new_group exercised three states (new group, same group, different group) in one method — split into three named tests, one per scenario.
AAA shape on a small Rust test:
#[test]
fn group_len_returns_zero_for_unknown_group() {
// arrange
let store = GroupStore::new();
// act
let len = store.group_len(&GroupId::from(42));
// assert
assert_eq!(len, 0);
}