
Test Quality Inspector
- 9 installs
- 145 repo stars
- Updated July 27, 2026
- bobmatnyc/claude-mpm
test-quality-inspector is a skill that inspects a test for semantic correctness, verifying whether it actually tests what it claims and would fail if the implementation broke.
About
This skill inspects tests for semantic correctness, verifying whether a test actually tests what it claims rather than just passing. It applies five checks (name-to-assertion alignment, meaningful assertions, mutation failure, edge case coverage, mock hygiene) and issues one of four verdicts with evidence and suggested fixes. A developer uses it when reviewing tests in a PR or when a test passes but a bug still shipped.
- Inspects whether a test actually verifies the behavior it claims, in any language
- Applies five checks: name-to-assertion, meaningful assertions, mutation failure, edge cases, mock hygiene
- Produces a verdict (CORRECT, MISLEADING, INCOMPLETE, BROKEN) with evidence and concrete fixes
Test Quality Inspector by the numbers
- 9 all-time installs (skills.sh)
- Ranked #1,560 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
test-quality-inspector capabilities & compatibility
- Capabilities
- webapp testing · test quality inspector
- Use cases
- testing · code review
- Pricing
- Free
What test-quality-inspector says it does
A passing test is not the same as a good test.
Would the assertion pass even if the implementation returned garbage?
Issue one of four verdicts with specific evidence.
npx skills add https://github.com/bobmatnyc/claude-mpm --skill test-quality-inspectorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 145 |
| Last updated | July 27, 2026 |
| Repository | bobmatnyc/claude-mpm ↗ |
What it does
Inspect a test to verify it meaningfully tests its named behavior and would catch real bugs.
Who is it for?
Reviewing new or modified tests in a PR to confirm they have meaningful assertions and would catch real bugs.
Skip if: Writing new tests from scratch or running a test suite.
When should I use this skill?
When reviewing a test file or suite to verify it has meaningful assertions and would fail if the implementation broke.
What you get
A verdict on each test with evidence and concrete fixes for misleading, incomplete, or broken tests.
- A verdict per test with evidence and concrete fixes
By the numbers
- Five-step inspection process
- Five checks and four verdicts
- Examples in Python, JavaScript, Go, and Java
Files
Test Quality Inspector
Overview
A passing test is not the same as a good test. This skill inspects tests for semantic correctness — whether the test actually verifies the behavior it claims to verify, not just whether it runs without error.
Apply this skill to any test file or suite, in any language or framework.
When to Use
Activate when:
- Reviewing a PR that includes new or modified tests
- A test passes but a bug still shipped
- Test names feel mismatched to their assertions
- Mocks seem unusually extensive
- Coverage numbers look good but confidence is low
- Preparing to refactor and needing to trust the test harness
Arguments
This skill accepts optional arguments:
- File path:
path/to/test_file.py— inspect a specific file - Test name pattern:
test_user_*— inspect tests matching the pattern - No args: inspect all tests in the current context or most recently discussed test
Five-Step Inspection Process
Step 1: Read the Test
Read the test file completely. Identify:
- The test name and any docstring or description
- What the test sets up (fixtures, mocks, data)
- What action it performs (the "act")
- What it asserts (the "assert")
- What it does NOT assert
Step 2: Read the Implementation
Find and read the actual code being tested. Identify:
- The function/method signature
- All return values and side effects
- Branches and edge cases in the implementation
- What could realistically go wrong
Step 3: Apply the Five Checks
Run all five checks. See checks.md for detailed guidance.
Check 1 — Name-to-Assertion Alignment Does the test name describe what the assertions actually verify? A test named test_returns_empty_list_when_no_results that only asserts len(result) == 0 without checking the type is subtly misleading.
Check 2 — Meaningful Assertions (No Tautologies) Would the assertion pass even if the implementation returned garbage? Examples of hollow assertions:
assert result is not Nonewhen the function always returns an objectassert len(result) >= 0(always true for lists)assertTrue(True)
Check 3 — Mutation Failure Check If the implementation were deliberately broken in the most obvious way (wrong return value, off-by-one, missing branch), would this test catch it? Mentally apply one mutation at a time and ask: does the test fail?
Check 4 — Edge Case Coverage If the test name references edge cases ("when empty", "when None", "at boundary"), verify those conditions are actually set up in the arrange phase and exercised in the act phase.
Check 5 — Mock Hygiene Are mocks replacing so much real behavior that the test no longer exercises the code under test? Signs of hollow mocking:
- The function under test is itself mocked
- All dependencies are stubbed with hardcoded return values that match the assertion exactly
- No real logic runs between the mock setup and the assertion
Step 4: Produce a Verdict
Issue one of four verdicts with specific evidence. See verdicts.md for verdict criteria and templates.
| Verdict | Meaning |
|---|---|
| CORRECT | Test accurately names its behavior, assertions are meaningful, would catch real bugs |
| MISLEADING | Test passes but the name or description does not match what is actually asserted |
| INCOMPLETE | Test covers some of the claimed behavior but misses important assertions or edge cases |
| BROKEN | Test would not catch an obvious bug in the code it claims to test |
Step 5: Suggest Fixes
For any verdict other than CORRECT, provide:
- The specific line(s) causing the issue
- A concrete example of how to fix it
- If applicable, an example of a bug the current test would fail to catch
Quick Check Summary
Would this test FAIL if I:
- Changed the return value to None? → Check assertions
- Removed the main branch logic? → Check coverage
- Swapped two arguments in the call? → Check specificity
- Deleted the function entirely? → Check mock depth
- Added a new edge case to the spec? → Check name accuracyRed Flags — STOP and Inspect
Stop and apply full inspection when:
- Test has no assertions (or only
assert True) - Every dependency is mocked
- Assertion checks a value that the mock itself returns
- Test name mentions a condition that doesn't appear in the arrange phase
- Test passes with an empty implementation
- Multiple behaviors tested in one test with a vague name
Navigation
- [Checks Reference](references/checks.md) — Detailed guide for all five checks with examples in Python, JavaScript, Go, and Java
- [Verdicts and Templates](references/verdicts.md) — Verdict criteria, evidence format, and report templates
- [Mock Hygiene](references/mock-hygiene.md) — When mocking is appropriate vs. when it hollows out a test
- [Mutation Reasoning](references/mutation-reasoning.md) — How to apply mutation-testing mindset without a mutation framework
Related Skills
- universal-testing-test-driven-development — Write tests correctly from the start
- universal-debugging-verification-before-completion — Verify your own work before claiming completion
- universal-testing-testing-anti-patterns — Broader catalog of test design mistakes
{
"name": "test-quality-inspector",
"version": "1.0.0",
"category": "universal",
"toolchain": null,
"framework": null,
"tags": [
"testing",
"quality-assurance",
"test-review",
"mutation-testing",
"mocking",
"semantic-correctness"
],
"entry_point_tokens": 95,
"full_tokens": 4800,
"author": "bobmatnyc",
"license": "Apache-2.0",
"requires": [],
"updated": "2026-04-29",
"source_path": "testing/test-quality-inspector/SKILL.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2026-04-29",
"modified": "2026-04-29",
"maintainer": "Claude MPM Team",
"attribution_required": false,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}
Five Checks — Detailed Reference
Detailed guidance for each of the five inspection checks, with examples in Python, JavaScript, Go, and Java.
---
Check 1: Name-to-Assertion Alignment
Question: Does the test name accurately describe what the assertions verify?
What to look for
Read the test name as a specification sentence: "This test verifies that [name]". Then read the assertions. Do the assertions actually verify what the name claims?
Common mismatches
Name is too broad:
# Name claims to test "user authentication"
def test_user_authentication():
user = User(email="a@b.com", password="x")
assert user.email == "a@b.com" # Only checks attribute storage, not authVerdict: MISLEADING — the test verifies attribute assignment, not authentication.
Name references a condition that isn't set up:
// Name says "when user is anonymous"
test('should return default permissions when user is anonymous', () => {
const user = new User({ id: 1, role: 'viewer' }); // Not anonymous!
const perms = getPermissions(user);
expect(perms).toEqual(DEFAULT_PERMISSIONS);
});Verdict: MISLEADING — the user is not anonymous; the test passes by coincidence.
Name describes a side effect but test only checks return value:
func TestShouldSaveUserToDatabase(t *testing.T) {
user := createUser("alice@example.com")
// Only checks return value, never verifies the DB write
assert.Equal(t, "alice@example.com", user.Email)
}Verdict: INCOMPLETE — no assertion verifies the database write.
Fix pattern
Either rename the test to match what it actually tests, or add assertions that cover what the name promises.
---
Check 2: Meaningful Assertions (No Tautologies)
Question: Would the assertion pass even with a broken or trivial implementation?
Always-true assertions
# Always true for any list
assert len(result) >= 0
# Always true when result is a list (even empty)
assert isinstance(result, list)
# Always true when function doesn't crash
assert result is not NoneAssertions that match mock return values exactly
// The mock returns { id: 1, name: 'Alice' }
jest.mock('./userService', () => ({
getUser: jest.fn().mockReturnValue({ id: 1, name: 'Alice' })
}));
test('should return user data', () => {
const result = fetchUserProfile(1);
expect(result).toEqual({ id: 1, name: 'Alice' }); // Just echoes the mock
});Verdict: BROKEN — the test only verifies that a mock returns what you told it to return. No real logic is tested.
Specificity too low
@Test
public void testCalculateTax() {
double result = taxCalculator.calculate(100.0);
assertTrue(result > 0); // Would pass even if result = 0.01 or 99.99
}Better: assertEquals(8.5, result, 0.001) — checks the actual expected value.
Fix pattern
Replace vague range assertions with specific value checks. Replace existence checks (!= null) with value checks. If you don't know the exact expected value, that's a sign the test was written after the implementation without thinking about expected behavior.
---
Check 3: Mutation Failure Check
Question: If the implementation were deliberately broken, would this test catch it?
Apply these mutations mentally
For a function def add(a, b): return a + b, the mutations are:
- Return
a - binstead ofa + b - Return
ainstead ofa + b - Return
0 - Return
None
A good test catches all of them:
def test_add_two_positive_numbers():
assert add(3, 4) == 7 # Catches all four mutations aboveA weak test:
def test_add_returns_a_number():
result = add(3, 4)
assert isinstance(result, int) # Passes for add(3,4)=0 or add(3,4)=3Common mutation survivors (bugs the test wouldn't catch)
Off-by-one goes undetected:
def test_paginate():
items = list(range(10))
result = paginate(items, page=1, size=3)
assert len(result) == 3 # Passes even if page indexing is off by oneMissing assertion: verify the content of the first page, not just the length.
Wrong branch taken silently:
// Implementation has a bug: uses >= instead of >
function isAdult(age) { return age >= 18; } // Bug: should be > 17
test('should return true for adults', () => {
expect(isAdult(21)).toBe(true); // Passes for both >= 18 and > 17
});Missing test: expect(isAdult(17)).toBe(false) — the boundary case.
Fix pattern
Test boundary values. Test the negative case alongside the positive case. Assert specific output values, not just types or existence.
---
Check 4: Edge Case Coverage
Question: If the test name mentions an edge case, is that case actually exercised?
"When empty" not actually empty
def test_process_items_when_list_is_empty():
items = [None] # Not empty! None is a value.
result = process_items(items)
assert result == []"When None" not actually None
test('should handle null userId gracefully', () => {
const result = getUser(undefined); // undefined != null in JS
expect(result).toBeNull();
});"At boundary" not at the boundary
func TestShouldRejectStringsLongerThan100Chars(t *testing.T) {
// Tests with 50 chars, not 100 or 101
input := strings.Repeat("a", 50)
err := validate(input)
assert.NoError(t, err)
}Missing: test with exactly 100 chars (should pass), exactly 101 chars (should fail).
Fix pattern
Read the arrange section carefully. Verify the test data actually represents the condition named. For boundary tests, test both sides of the boundary.
---
Check 5: Mock Hygiene
Question: Do mocks remove so much real behavior that the test no longer exercises the code under test?
See mock-hygiene.md for full details. Key patterns here:
The function under test is itself mocked
# Testing process_order, but process_order is mocked
with patch('mymodule.process_order') as mock_process:
mock_process.return_value = {'status': 'ok'}
result = process_order(order_data)
assert result['status'] == 'ok'Verdict: BROKEN — this test verifies nothing about the real process_order.
All inputs and outputs are controlled by the mock
const mockDb = {
findUser: jest.fn().mockReturnValue({ id: 1, balance: 100 }),
updateBalance: jest.fn().mockReturnValue({ id: 1, balance: 50 })
};
test('should deduct amount from balance', () => {
const result = deductBalance(mockDb, 1, 50);
expect(result.balance).toBe(50);
});If deductBalance is (db, id, amount) => db.updateBalance(id, { balance: db.findUser(id).balance - amount }), the test only verifies the mock chain, not the subtraction logic.
Fix pattern
Mock external I/O (network, database, file system). Do not mock the function under test or its core logic. Verify that real computation happens between mock calls.
Mock Hygiene
When does mocking help, and when does it hollow out a test?
The Core Principle
Mocks exist to isolate the code under test from external systems (network, database, file system, time). They should never replace the code under test itself or its core logic.
Good mocking: Replace the thing that makes testing hard. Bad mocking: Replace the thing you're supposed to be testing.
---
What Should Be Mocked
External I/O (Always appropriate)
- HTTP clients / API calls
- Database connections and queries
- File system reads and writes
- Time (
datetime.now(),Date.now()) - Random number generators
- Email / SMS / notification senders
- Message queue producers and consumers
Expensive or non-deterministic computations
- ML model inference
- Cryptographic key generation
- External rate-limited services
Other service boundaries (unit test context)
- Other microservices
- Third-party SDKs
- Browser APIs in server tests
---
What Should NOT Be Mocked
The function under test
# BROKEN: You are testing the mock, not the function
with patch('mymodule.calculate_discount') as mock:
mock.return_value = 0.1
result = calculate_discount(order)
assert result == 0.1This test proves nothing. Remove it or rewrite it to test real logic.
Core business logic called by the function under test
// BROKEN: discountService contains the logic being tested
const mockDiscountService = {
apply: jest.fn().mockReturnValue(90.00)
};
test('should apply discount to order total', () => {
const result = applyOrderDiscount(order, mockDiscountService);
expect(result.total).toBe(90.00);
});If applyOrderDiscount is just (order, service) => service.apply(order), then mocking service.apply means no logic is tested.
Data transformations that are the point of the test
# BROKEN: The serialization IS what we're testing
with patch('mymodule.serialize_user') as mock_serialize:
mock_serialize.return_value = {'id': 1, 'name': 'Alice'}
result = format_user_response(user)
assert result == {'id': 1, 'name': 'Alice'}---
Signs a Mock is Hollowing Out a Test
Sign 1: The assertion matches the mock return value exactly
mockDb.getUser.mockReturnValue({ id: 1, name: 'Alice', role: 'admin' });
// Assertion just echoes the mock
expect(result).toEqual({ id: 1, name: 'Alice', role: 'admin' });If you change the mock return value and the assertion breaks — but the production code didn't change — the test is just verifying the mock configuration.
Sign 2: No real code runs between the mock setup and the assertion
Count the lines between mock.returnValue(X) and expect(result).toBe(Y). If there are only 1-2 lines and neither calls real application logic, the test is empty.
Sign 3: The mock depth is greater than 1
# Suspicious: mocking a method on a mock
mock_repo = Mock()
mock_repo.find_by_id.return_value = Mock(email="a@b.com", is_active=True)Deeply nested mocks often indicate that the test has lost contact with the actual code path.
Sign 4: Mocking something that doesn't have I/O
mockCalculator := &MockCalculator{}
mockCalculator.On("Add", 3, 4).Return(7)
result := processNumbers(mockCalculator, 3, 4)
assert.Equal(t, 7, result)If Calculator.Add is a pure function with no I/O, why mock it? The test proves nothing about processNumbers.
---
Mock Depth Decision Tree
Is this component doing I/O (network, disk, time, randomness)?
YES → Mock is appropriate
NO →
Is this component a third-party library I don't own?
YES → Mock is appropriate (unit test scope)
NO →
Is this component the function I'm testing?
YES → Do NOT mock it. You're testing nothing.
NO →
Does this component contain the logic I'm verifying?
YES → Do NOT mock it. You're testing the mock.
NO → Mock is acceptable if isolation is needed.---
Appropriate vs. Hollow Mock Examples
Appropriate: I/O isolation
# Testing email validation logic; mocking the SMTP sender
def test_send_welcome_email_to_valid_address():
with patch('myapp.email.smtp_client.send') as mock_send:
send_welcome_email("alice@example.com", "Alice")
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome, Alice!",
body=ANY
)Real: Email composition logic. Mocked: SMTP network call.
Hollow: Logic replaced
# Testing email validation; but the validation itself is mocked
def test_validates_email_format():
with patch('myapp.email.validate_email_format') as mock_validate:
mock_validate.return_value = True
result = send_welcome_email("not-an-email", "Alice")
assert result.success is TrueThe test can never fail because the validation that should catch bad emails is bypassed.
Appropriate: Service boundary in unit test
// Unit test for OrderProcessor; UserService is a separate service
const mockUserService = {
getUser: jest.fn().mockResolvedValue({ id: 1, creditLimit: 500 })
};
test('should reject order if total exceeds credit limit', async () => {
const order = { userId: 1, total: 600 };
await expect(processOrder(order, mockUserService))
.rejects.toThrow('Exceeds credit limit');
});Real: credit limit check logic in processOrder. Mocked: the external UserService call.
---
Fixing Hollow Tests
When you find a test where mocking has hollowed out the logic:
1. Identify what real code should run — what computation, transformation, or decision is the test supposed to cover?
2. Move the mock further out — mock the I/O boundary, not the logic layer.
3. Use a fake instead of a mock — implement a minimal in-memory version of the dependency that exercises the real code path.
4. Write an integration test — if the logic is inseparable from the I/O, write an integration test with a real (test) database or in-process server.
Mutation Reasoning — Testing Without a Framework
How to apply mutation-testing thinking manually, without running a mutation framework like mutmut, PIT, or Stryker.
What Is Mutation Testing?
Mutation testing systematically introduces small bugs into source code ("mutants") and then checks whether the existing test suite detects them. If a mutant survives (tests still pass with the bug present), the test suite has a gap.
This reference teaches you to reason through mutations mentally during a code review — no tooling required.
---
Standard Mutation Operators
These are the changes a mutation framework would make. Apply them mentally to the implementation when inspecting a test.
Arithmetic mutations
| Original | Mutant |
|---|---|
a + b | a - b |
a * b | a / b |
a % b | a * b |
a ** b | a * b |
Test implication: Assertions must use specific numeric values that distinguish the correct result from the wrong arithmetic.
# Weak: survives arithmetic mutation
assert result > 0 # True whether result is 7 or -7
# Strong: kills arithmetic mutation
assert result == 7 # Fails if + becomes -Conditional mutations
| Original | Mutant |
|---|---|
x > y | x >= y, x < y, x == y, True, False |
x == y | x != y, True, False |
x and y | x or y, x, y, True, False |
Test implication: Test both sides of every boundary. If the condition is age >= 18, test both age == 17 (should fail) and age == 18 (should pass).
# Kills the >= → > mutation
def test_allows_exactly_18():
assert is_adult(18) is True
def test_rejects_17():
assert is_adult(17) is FalseReturn value mutations
| Original | Mutant |
|---|---|
return value | return None |
return True | return False |
return [] | return None |
return obj | return new empty instance |
Test implication: Assertions must check the actual return value, not just that something was returned.
# Weak: survives return None mutation
assert result is not None
# Strong: kills return None mutation
assert result == expected_valueStatement deletion mutations
The mutant simply removes a line. Common targets:
- A side-effect call (logging, database write, email send)
- A validation check
- A list append or dict update
Test implication: For side effects named in the test, assert they occurred. For validation, test that invalid input is actually rejected.
# Missing: the side-effect assertion
def test_user_created_successfully():
create_user("alice@example.com")
user = db.find_by_email("alice@example.com")
assert user is not None # Also catches the deletion mutation
# Or with mocks:
mock_db.save.assert_called_once() # Catches deletion of the save callException handling mutations
| Original | Mutant |
|---|---|
raise ValueError("...") | Remove the raise |
except ValueError: | except Exception: (catches too much) |
Test implication: For error-path tests, verify the exact exception type and message, not just that some exception occurred.
# Weak: survives mutating ValueError to RuntimeError
with pytest.raises(Exception):
process_invalid_input(data)
# Strong: kills the mutation
with pytest.raises(ValueError, match="invalid format"):
process_invalid_input(data)---
Mental Mutation Checklist
When inspecting a test, mentally apply these mutations to the function under test, then check whether the test's assertions would detect each one:
For each key operation in the implementation:
Arithmetic:
[ ] Change + to - (or * to /)
[ ] Would the assertion value change? If no → BROKEN
Conditions:
[ ] Flip > to >= (or change == to !=)
[ ] Does a boundary test cover this? If no → likely INCOMPLETE
Return values:
[ ] Return None instead of the computed value
[ ] Does an assertion check the value specifically? If no → BROKEN
Side effects:
[ ] Delete the side-effect call (DB write, event emit, etc.)
[ ] Does an assertion verify the side effect occurred? If no → INCOMPLETE
Error paths:
[ ] Remove the validation / raise
[ ] Is there a test that sends invalid input and expects an error? If no → INCOMPLETE---
Examples by Language
Python — missed arithmetic mutation
def calculate_compound_interest(principal, rate, periods):
return principal * (1 + rate) ** periods
# Test that does NOT kill the arithmetic mutation
def test_compound_interest():
result = calculate_compound_interest(1000, 0.05, 2)
assert result > 1000 # True even if ** becomes *
# Test that DOES kill it
def test_compound_interest():
result = calculate_compound_interest(1000, 0.05, 2)
assert abs(result - 1102.50) < 0.01 # Fails for * mutation (gives 1100)JavaScript — missed conditional mutation
function getDiscount(age) {
if (age >= 65) return 0.15;
if (age >= 18) return 0.05;
return 0;
}
// Does NOT kill the >= 65 → > 65 mutation
test('senior discount', () => {
expect(getDiscount(70)).toBe(0.15); // True for both >= 65 and > 65
});
// Kills it
test('senior discount at exact boundary', () => {
expect(getDiscount(65)).toBe(0.15); // Fails if >= 65 becomes > 65
expect(getDiscount(64)).toBe(0.05); // Also verifies the boundary is correct
});Go — missed side-effect mutation
func SaveUser(db Database, user User) error {
if err := db.Insert(user); err != nil {
return err
}
return nil
}
// Does NOT kill the deletion of db.Insert
func TestSaveUser(t *testing.T) {
db := &MockDatabase{}
err := SaveUser(db, User{Name: "Alice"})
assert.NoError(t, err) // Passes even if Insert is deleted (returns nil)
}
// Kills it
func TestSaveUser(t *testing.T) {
db := &MockDatabase{}
err := SaveUser(db, User{Name: "Alice"})
assert.NoError(t, err)
assert.Equal(t, 1, db.InsertCallCount()) // Fails if Insert is deleted
}Java — missed return value mutation
public String formatName(String first, String last) {
return last + ", " + first;
}
// Does NOT kill return null mutation
@Test
public void testFormatName() {
String result = formatName("John", "Doe");
assertNotNull(result); // Passes for "Doe, John" or "John Doe" or any non-null
}
// Kills it
@Test
public void testFormatName() {
String result = formatName("John", "Doe");
assertEquals("Doe, John", result); // Fails for any mutation
}---
When to Stop Applying Mutations
Not every mutation needs a test. Prioritize mutations in:
1. Core business logic — discount calculations, permission checks, state transitions 2. Boundary conditions — off-by-one errors are among the most common bugs 3. Error paths — missing error handling causes production incidents 4. Side effects with consequences — database writes, email sends, financial transactions
Lower priority:
- Pure logging statements
- Debug-only code paths
- Trivial getters and setters
- Code that is already covered by integration tests
Verdicts and Report Templates
Verdict Criteria
CORRECT
Issue: All five checks pass.
Criteria:
- Test name accurately describes the behavior verified
- Assertions check specific, meaningful values
- At least one mutation would cause the test to fail
- Edge cases named in the test are present in the arrange phase
- Mocks are limited to external I/O and do not hollow out logic
Report format:
Verdict: CORRECT
Test: test_should_return_discounted_price_when_coupon_is_valid
Evidence:
- Name matches assertions: checks that price == original * 0.9 when coupon code 'SAVE10' applied
- Assertion is specific: assertEqual(90.0, result) not just assertLess(result, 100)
- Mutation check: changing multiplier from 0.9 to 0.8 in implementation would fail this test
- Edge case coverage: coupon 'SAVE10' is explicitly set up in arrange
- Mock hygiene: only database read is mocked; discount calculation runs real code---
MISLEADING
Issue: Check 1 (Name-to-Assertion Alignment) fails. The test passes but the name or description does not match what is actually tested.
Criteria (any one sufficient):
- Test name describes behavior X but assertions only verify Y
- Test name references a condition that is not set up in the arrange phase
- Test name implies a negative case but the test only checks the positive path
- Test docstring contradicts what the assertions verify
Report format:
Verdict: MISLEADING
Test: test_should_reject_expired_tokens
Issue: The test name claims to verify that expired tokens are rejected, but the
token created in the arrange phase never has an expiry date set. The assertion
checks that `result.user_id == 1`, which would pass for any valid token.
Evidence:
Line 12: token = Token(user_id=1, value="abc123")
→ No expiry field set; this is a valid, non-expired token
Line 18: assert result.user_id == 1
→ This assertion passes for valid tokens; it does not test rejection
Suggested fix:
token = Token(user_id=1, value="abc123", expires_at=datetime.now() - timedelta(hours=1))
with pytest.raises(TokenExpiredError):
validate_token(token)---
INCOMPLETE
Issue: Checks 1, 4, or a combination are partially satisfied. The test covers some behavior but misses important assertions or edge cases.
Criteria (any one sufficient):
- Test verifies the happy path but does not assert error conditions named in the test
- Test checks return value but not documented side effects
- Edge cases in the test name are missing from the arrange phase
- Assertions cover some fields but leave critical fields unchecked
Report format:
Verdict: INCOMPLETE
Test: test_should_send_welcome_email_when_user_registers
Issue: The test verifies that user registration returns a success response but does
not verify that a welcome email was sent. The test name explicitly promises email
verification.
Evidence:
Assertions present:
assert result.status == 'created'
assert result.user.id is not None
Missing assertion:
No check that email service was called with the new user's address
Suggested fix:
mock_email_service.send_welcome.assert_called_once_with(
to="alice@example.com",
name="Alice"
)---
BROKEN
Issue: Check 3 (Mutation Failure) fails. An obvious bug in the implementation would not be caught by this test.
Criteria (any one sufficient):
- Test asserts only type or existence, not value
- All assertions match mock return values exactly (no real logic verified)
- The function under test is itself mocked
- Test passes with a stub implementation that returns a hardcoded value
- Assertion is tautological (always true)
Report format:
Verdict: BROKEN
Test: test_calculate_order_total
Issue: The test would pass even if calculate_order_total() returned 0 or any positive
number. The assertion only checks that the result is a float, not that it equals the
correct total.
Evidence:
Line 24: assert isinstance(result, float)
→ Passes for result = 0.0, result = 999.99, result = correct_value
Mutation that would NOT be caught:
Changing `return sum(item.price for item in items)` to `return 0.0`
→ Test still passes
Suggested fix:
items = [Item(price=10.00), Item(price=5.50)]
result = calculate_order_total(items)
assert result == 15.50 # Specific expected value---
Reporting Multiple Tests
When inspecting a test suite or file, produce a summary table followed by individual reports for non-CORRECT tests only.
Summary table format
Test Quality Inspection Report
File: tests/test_billing.py
Inspected: 8 tests
| Test Name | Verdict | Primary Issue |
|----------------------------------------------|------------|-----------------------|
| test_charge_card_success | CORRECT | |
| test_charge_card_declined | CORRECT | |
| test_refund_full_amount | INCOMPLETE | Missing DB side effect|
| test_refund_partial_amount | MISLEADING | Name/assertion mismatch|
| test_calculate_tax_standard_rate | BROKEN | Always-true assertion |
| test_calculate_tax_exempt_items | CORRECT | |
| test_invoice_generation | INCOMPLETE | Missing format check |
| test_invoice_attachment_email | BROKEN | Function under test mocked|
Summary: 3 CORRECT, 2 INCOMPLETE, 1 MISLEADING, 2 BROKEN
Confidence in test suite: LOW — 5 of 8 tests would not catch bugs they claim to coverThen individual reports for the 5 non-CORRECT tests.
---
Confidence Rating
After inspecting a suite, provide a confidence rating:
HIGH: 90%+ of tests are CORRECT. Mutations in the implementation would reliably be caught.
MEDIUM: 70-89% CORRECT. Most bugs would be caught. Some edge cases could slip through.
LOW: 50-69% CORRECT. Significant gaps. A developer could introduce bugs without tests failing.
CRITICAL: <50% CORRECT. Test suite provides false confidence. Do not rely on it for refactoring or regression detection.
Related skills
FAQ
What does the inspector check?
Five checks: name-to-assertion alignment, meaningful assertions (no tautologies), mutation failure, edge case coverage, and mock hygiene.
What verdicts can it produce?
CORRECT, MISLEADING, INCOMPLETE, or BROKEN, each with specific evidence and, for non-CORRECT verdicts, concrete fixes.