TEST-002 Keep tests small and focused
Each unit test should verify one specific behavior. Avoid exercising multiple behaviors or features within a single test case.
Tip
A useful heuristic: if you cannot describe what a test checks without using “and”, it is testing more than one behavior and should be split.Motivation
When a test verifies several behaviors at once, a failure only tells you that something in that group broke, not which behavior. You then have to read the test and reproduce its steps to find the cause. A test scoped to one behavior names the failure on its own: the test that broke is the behavior that broke.
Broad tests also fail for more reasons; a test that touches several behaviors breaks whenever any of them changes, including changes unrelated to what the test was meant to protect. This produces failures that are noise rather than signal, and the test gets edited or ignored instead of trusted.
Justification
The value of a test is the precision of the signal it gives when it fails. One behavior per test maximizes that precision: a red test maps to a single, identified behavior, so debugging starts from a known cause instead of a search. It also keeps the test’s reasons to fail aligned with its purpose – it changes when its behavior changes, and not otherwise – which is what makes a suite worth keeping green.
Focused tests compose better as documentation. A reader scanning the test names sees an enumerated list of behaviors the unit guarantees. A test that bundles several behaviors hides them behind one name and that list becomes incomplete.
Examples
C++ example
❌ Bad Example
// One test, two behaviors. A failure does not say which.
TEST_CASE("account operations") {
Account account{/*balance=*/100};
account.deposit(50);
REQUIRE(account.balance() == 150);
account.withdraw(40);
REQUIRE(account.balance() == 110);
}
✅ Good Example
// One behavior per test.
TEST_CASE("deposit increases the balance") {
// Arrange
Account account{/*balance=*/100};
// Act
account.deposit(50);
// Assert
REQUIRE(account.balance() == 150);
}
TEST_CASE("withdraw reduces the balance") {
// Arrange
Account account{/*balance=*/100};
// Act
account.withdraw(40);
// Assert
REQUIRE(account.balance() == 60);
}
Go example
❌ Bad Example
// One test, two behaviors. A failure does not say which.
func TestAccountOperations(t *testing.T) {
account := NewAccount(100)
account.Deposit(50)
if got, want := account.Balance(), 150; got != want {
t.Errorf("Balance() = %d, want %d", got, want)
}
account.Withdraw(40)
if got, want := account.Balance(), 110; got != want {
t.Errorf("Balance() = %d, want %d", got, want)
}
}
✅ Good Example
// One behavior per test.
func TestDepositIncreasesBalance(t *testing.T) {
t.Parallel()
// Arrange
account := NewAccount(100)
// Act
account.Deposit(50)
// Assert
if got, want := account.Balance(), 150; got != want {
t.Errorf("Balance() = %d, want %d", got, want)
}
}
func TestWithdrawReducesBalance(t *testing.T) {
t.Parallel()
// Arrange
account := NewAccount(100)
// Act
account.Withdraw(40)
// Assert
if got, want := account.Balance(), 60; got != want {
t.Errorf("Balance() = %d, want %d", got, want)
}
}
Python example
❌ Bad Example
# One test, two behaviors. A failure does not say which.
def test_account_operations():
account = Account(balance=100)
account.deposit(50)
assert account.balance() == 150
account.withdraw(40)
assert account.balance() == 110
✅ Good Example
# One behavior per test.
def test_deposit_increases_balance():
# Arrange
account = Account(balance=100)
# Act
account.deposit(50)
# Assert
assert account.balance() == 150
def test_withdraw_reduces_balance():
# Arrange
account = Account(balance=100)
# Act
account.withdraw(40)
# Assert
assert account.balance() == 60
Rust example
❌ Bad Example
// One test, two behaviors. A failure does not say which.
#[test]
fn account_operations() {
let mut account = Account::new(100);
account.deposit(50);
assert_eq!(account.balance(), 150);
account.withdraw(40);
assert_eq!(account.balance(), 110);
}
✅ Good Example
// One behavior per test.
#[test]
fn deposit_increases_balance() {
// Arrange
let mut account = Account::new(100);
// Act
account.deposit(50);
// Assert
assert_eq!(account.balance(), 150);
}
#[test]
fn withdraw_reduces_balance() {
// Arrange
let mut account = Account::new(100);
// Act
account.withdraw(40);
// Assert
assert_eq!(account.balance(), 60);
}
Resources
X-Unit Test Patterns by Gerard Meszaros - a classic book on unit testing.
Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin - covers the single-responsibility principle as applied to tests, including one assertion concept per test.