ntcoding/writing-tests
Principles for writing effective, maintainable tests. Covers naming conventions, assertion best practices, and comprehensive edge case checklists. Based on BugMagnet by Gojko Adzic. Triggers on: writing any test, 'add tests', test review, test naming, assertion choices, edge case coverage, 'what should I test', test structure decisions.
npx skills add https://github.com/NTCoding/claude-skillz --skill writing-tests
How to write tests that catch bugs, document behavior, and remain maintainable.
> Based on BugMagnet by Gojko Adzic. Adapted with attribution.
🚨 Test names describe outcomes, not actions. "returns empty array when input is null" not "test null input". The name IS the specification.
🚨 Assertions must match test titles. If the test claims to verify "different IDs", assert on the actual ID values—not just count or existence.
🚨 Assert specific values, not types. expect(result).toEqual(['First.', ' Second.']) not expect(result).toBeDefined(). Specific assertions catch specific bugs.
🚨 One concept per test. Each test verifies one behavior. If you need "and" in your test name, split it.
🚨 Bugs cluster together. When you find one bug, test related scenarios. The same misunderstanding often causes multiple failures.
Pattern: [outcome] when [condition]
returns empty array when input is null
throws ValidationError when email format invalid
calculates tax correctly for tax-exempt items
preserves original order when duplicates removed
test null input // What about null input?
should work // What does "work" mean?
handles edge cases // Which edge cases?
email validation test // What's being validated?
Your test name should read like a specification. If someone reads ONLY the test names, they should understand the complete behavior of the system.
// ❌ WEAK - passes even if completely wrong data
expect(result).toBeDefined()
expect(result.items).toHaveLength(2)
expect(user).toBeTruthy()
// ✅ STRONG - catches actual bugs
expect(result).toEqual({ status: 'success', items: ['a', 'b'] })
expect(user.email).toBe('[email protected]')
// ❌ TEST SAYS "different IDs" BUT ASSERTS COUNT
it('generates different IDs for each call', () => {
const id1 = generateId()
const id2 = generateId()
expect([id1, id2]).toHaveLength(2) // WRONG: doesn't check they're different!
})
// ✅ ACTUALLY VERIFIES DIFFERENT IDs
it('generates different IDs for each call', () => {
const id1 = generateId()
const id2 = generateId()
expect(id1).not.toBe(id2) // RIGHT: verifies the claim
})
// ❌ BRITTLE - tests implementation details
expect(mockDatabase.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = 1')
// ✅ FLEXIBLE - tests behavior
expect(result.user.name).toBe('Alice')
it('calculates total with tax for non-exempt items', () => {
// Arrange: Set up test data
const item = { price: 100, taxExempt: false }
const taxRate = 0.1
// Act: Execute the behavior
const total = calculateTotal(item, taxRate)
// Assert: Verify the outcome
expect(total).toBe(110)
})
// ❌ MULTIPLE CONCEPTS - hard to diagnose failures
it('validates and processes order', () => {
expect(validate(order)).toBe(true)
expect(process(order).status).toBe('complete')
expect(sendEmail).toHaveBeenCalled()
})
// ✅ SINGLE CONCEPT - clear failures
it('accepts valid orders', () => {
expect(validate(validOrder)).toBe(true)
})
it('rejects orders with negative quantities', () => {
expect(validate(negativeQuantityOrder)).toBe(false)
})
it('sends confirmation email after processing', () => {
process(order)
expect(sendEmail).toHaveBeenCalledWith(order.customerEmail)
})
When testing a function, systematically consider these edge cases based on input types.
""" "[], {}null inputundefined inputundefined vs missing keyThese test implicit assumptions in your domain:
When testing code that validates properties against type constraints (e.g., validating route: string in an interface):
Wrong-type literals:
route = 123)route = true)count = 'five')enabled = 'yes')Non-literal expressions:
route = /path/${id} )route = someVariable)route = getRoute())route = config.path)Correct type:
route = '/orders')'', zero 0, false)Why this matters:
A common bug pattern is validating "is this a literal?" without checking "is this the RIGHT TYPE of literal?"
hasLiteralValue() returns true for 123, true, and 'string'hasStringLiteralValue() returns true only for 'string'When an interface specifies property: string, validation must reject numeric and boolean literals, not just non-literal expressions.
When you discover a bug, don't stop—explore related scenarios:
toBeDefined() or toBeTruthy(): STOP. What value do you actually expect? Assert that instead.With TDD Process: This skill guides the RED phase—how to write the failing test well.
With Software Design Principles: Testable code follows design principles. Hard-to-test code often has design problems.
Take ntcoding/writing-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.