Testing Patterns
Write tests that catch bugs, not tests that pass. — Confidence through coverage, speed through isolation.
Testing Pyramid
| Level |
Ratio |
Speed |
Cost |
Confidence |
Scope |
| Unit |
~70% |
ms |
Low |
Low (isolated) |
Single function/class |
| Integration |
~20% |
seconds |
Medium |
Medium |
Module boundaries, APIs, DB |
| E2E |
~10% |
minutes |
High |
High (realistic) |
Full user workflows |
Rule: If your E2E tests outnumber your unit tests, invert the pyramid.
Unit Testing Patterns
Core Patterns
| Pattern |
When to Use |
Structure |
| Arrange-Act-Assert |
Default for all unit tests |
Setup, Execute, Verify |
| Given-When-Then |
BDD-style, behavior-focused |
Precondition, Action, Outcome |
| Parameterized |
Same logic, multiple inputs |
Data-driven test cases |
| Snapshot |
UI components, serialized output |
Compare against saved baseline |
| Property-Based |
Mathematical invariants |
Generate random inputs, assert properties |
Arrange-Act-Assert (AAA)
The default structure for every unit test. Clear separation of setup, execution, and verification makes tests readable and maintainable.
// Clean AAA structure
test('calculates order total with tax', () => {
// Arrange
const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }];
const taxRate = 0.08;
// Act
const total = calculateTotal(items, taxRate);
// Assert
expect(total).toBe(27.0);
});
Test Doubles
Use the right type of test double for the situation. Each serves a different purpose.
| Double |
Purpose |
When to Use |
Example |
| Stub |
Returns canned data |
Control indirect input |
jest.fn().mockReturnValue(42) |
| Mock |
Verifies interactions |
Assert something was called |
expect(mock).toHaveBeenCalledWith('arg') |
| Spy |
Wraps real implementation |
Observe without replacing |
jest.spyOn(service, 'save') |
| Fake |
Working simplified impl |
Need realistic behavior |
In-memory database, fake HTTP server |
// Stub — control indirect input
const getUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' });
// Spy — observe without replacing
const spy = jest.spyOn(logger, 'warn');
processInvalidInput(data);
expect(spy).toHaveBeenCalledWith('Invalid input received');
// Fake — lightweight substitute
class FakeUserRepo implements UserRepository {
private users = new Map<string, User>();
async save(user: User) { this.users.set(user.id, user); }
async findById(id: string) { return this.users.get(id) ?? null; }
}
Parameterized Tests
Use parameterized tests when the same logic needs verification with multiple inputs. This eliminates copy-paste tests while providing comprehensive coverage.
// Vitest/Jest
test.each([
['hello', 'HELLO'],
['world', 'WORLD'],
['', ''],
['123abc', '123ABC'],
])('toUpperCase(%s) returns %s', (input, expected) => {
expect(input.toUpperCase()).toBe(expected);
});
# pytest
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
])
def test_to_upper(input, expected):
assert input.upper() == expected
// Go — table-driven tests (idiomatic)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.expected {
t.Errorf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
Integration Testing Patterns
Database Testing Strategies
| Strategy |
Approach |
Trade-off |
| Transaction rollback |
Wrap each test in a transaction, rollback after |
Fast, but hides commit bugs |
| Fixtures/seeds |
Load known data before suite |
Predictable, but brittle if schema changes |
| Factory functions |
Generate data programmatically |
Flexible, but more setup code |
| Testcontainers |
Spin up real DB in Docker |
Realistic, but slower startup |
// Transaction rollback pattern (Prisma)
beforeEach(async () => {
await prisma.$executeRaw`BEGIN`;
});
afterEach(async () => {
await prisma.$executeRaw`ROLLBACK`;
});
test('creates user in database', async () => {
const user = await createUser({ name: 'Alice', email: 'a@b.com' });
const found = await prisma.user.findUnique({ where: { id: user.id } });
expect(found?.name).toBe('Alice');
});
API Testing
// Supertest (Node.js)
import request from 'supertest';
import { app } from '../src/app';
describe('POST /api/users', () => {
it('creates a user and returns 201', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@test.com' })
.expect(201);
expect(res.body).toMatchObject({
id: expect.any(String),
name: 'Alice',
});
});
it('returns 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'not-an-email' })
.expect(400);
});
});
Mocking Best Practices
Mock Boundaries, Not Implementations
The fundamental rule: mock at system boundaries (external APIs, databases, file systems) and never mock internal domain logic.
// BAD — mocking internal implementation
jest.mock('./utils/formatDate'); // Breaks on refactor
// GOOD — mocking external boundary
jest.mock('./services/paymentGateway'); // Third-party API is the boundary
When to Mock vs Not Mock
| Mock |
Don't Mock |
| HTTP APIs, external services |
Pure functions |
| Database (in unit tests) |
Your own domain logic |
| File system, network |
Data transformations |
Time/Date (Date.now) |
Simple calculations |
| Environment variables |
Internal class methods |
Dependency Injection for Testability
Structure code so dependencies can be swapped in tests. This is the single most impactful pattern for testable code.
// Injectable dependencies — easy to test
class OrderService {
constructor(
private paymentGateway: PaymentGateway,
private inventory: InventoryService,
private notifier: NotificationService,
) {}
async placeOrder(order: Order): Promise<OrderResult> {
const stock = await this.inventory.check(order.items);
if (!stock.available) return { status: 'out_of_stock' };
const payment = await this.paymentGateway.charge(order.total);
if (!payment.success) return { status: 'payment_failed' };
await this.notifier.send(order.userId, 'Order confirmed');
return { status: 'confirmed', id: payment.transactionId };
}
}
// In tests — inject fakes
const service = new OrderService(
new FakePaymentGateway(),
new FakeInventory({ available: true }),
new FakeNotifier(),
);
Framework Quick Reference
| Framework |
Language |
Type |
Test Runner |
Assertion |
| Jest |
JS/TS |
Unit/Integration |
Built-in |
expect() |
| Vitest |
JS/TS |
Unit/Integration |
Vite-native |
expect() (Jest-compatible) |
| Playwright |
JS/TS/Python |
E2E |
Built-in |
expect() / locators |
| Cypress |
JS/TS |
E2E |
Built-in |
cy.should() |
| pytest |
Python |
Unit/Integration |
Built-in |
assert |
| Go testing |
Go |
Unit/Integration |
go test |
t.Error() / testify |
| Rust |
Rust |
Unit/Integration |
cargo test |
assert!() / assert_eq!() |
| JUnit 5 |
Java/Kotlin |
Unit/Integration |
Built-in |
assertEquals() |
| RSpec |
Ruby |
Unit/Integration |
Built-in |
expect().to |
| PHPUnit |
PHP |
Unit/Integration |
Built-in |
$this->assert*() |
| xUnit |
C# |
Unit/Integration |
Built-in |
Assert.Equal() |
Framework Command Quick Reference
Use these commands as the default starting point before adding project-specific flags.
| Framework |
Install |
Common Commands |
| Vitest |
npm install -D vitest @testing-library/react @testing-library/jest-dom |
npx vitest, npx vitest run, npx vitest --coverage |
| Jest |
npm install -D jest @types/jest ts-jest |
npx jest, npx jest --watch, npx jest --coverage, npx jest path/to/test |
| pytest |
uv pip install pytest pytest-cov pytest-asyncio httpx |
pytest, pytest -v, pytest -x, pytest --cov=app, pytest tests/test_api.py -k "test_login" |
| XCTest |
Built into SwiftPM/Xcode |
swift test, swift test --filter MyTests, swift test --parallel |
| Playwright |
npm install -D @playwright/test && npx playwright install |
npx playwright test, npx playwright test --headed, npx playwright test --debug, npx playwright show-report |
Test Quality Checklist
| Quality |
Rule |
Why |
| Deterministic |
Same input produces same result, every time |
Flaky tests erode trust |
| Isolated |
No shared mutable state between tests |
Order-dependent tests break in CI |
| Fast |
Unit: < 10ms, Integration: < 1s, E2E: < 30s |
Slow tests don't get run |
| Readable |
Test name describes the scenario and expectation |
Tests are documentation |
| Maintainable |
Change one behavior, change one test |
Brittle tests slow development |
| Focused |
One logical assertion per test |
Failures pinpoint the problem |
Naming convention: test_[unit]_[scenario]_[expected result] or should [do X] when [condition Y]
Coverage Strategy
When to Aim for What
| Target |
When |
Rationale |
| 80%+ line coverage |
Business logic, utilities, core domain |
High ROI — catches most regressions |
| 90%+ branch coverage |
Payment processing, auth, security-critical |
Edge cases matter here |
| 100% coverage |
Almost never — diminishing returns |
Getter/setter tests add noise, not confidence |
| Mutation testing |
Critical paths after coverage is high |
Verifies tests actually catch bugs |
What NOT to Test
| Skip |
Reason |
| Generated code (Prisma client, protobuf) |
Maintained by tooling |
| Third-party library internals |
Not your responsibility |
| Simple getters/setters |
No logic to verify |
| Configuration files |
Test the behavior they configure instead |
| Console.log / print statements |
Side effects with no business value |
Test Organization
src/
├── services/
│ ├── order.service.ts
│ └── order.service.test.ts # Co-located unit tests
├── api/
│ └── routes/
│ └── orders.ts
tests/
├── integration/
│ ├── api/
│ │ └── orders.test.ts # API integration tests
│ └── db/
│ └── order.repo.test.ts # DB integration tests
├── e2e/
│ ├── pages/ # Page objects
│ │ └── checkout.page.ts
│ └── specs/
│ └── checkout.spec.ts # E2E specs
└── helpers/
├── factories.ts # Test data factories
└── setup.ts # Global test setup
Rule: Co-locate unit tests with source. Separate integration and E2E tests into dedicated directories.
Anti-Patterns
| Anti-Pattern |
Problem |
Fix |
| Testing implementation |
Tests break on refactor, not on bugs |
Test behavior and outputs, not internals |
| Flaky tests |
Non-deterministic failures erode CI trust |
Remove time/order/network dependencies |
| Test pollution |
Shared mutable state leaks between tests |
Reset state in beforeEach / setUp |
| Sleeping in tests |
sleep(2000) is slow and unreliable |
Use explicit waits, polling, or events |
| Giant arrange |
50 lines of setup obscure intent |
Extract factories/builders/fixtures |
| Assert-free tests |
Test runs but verifies nothing |
Every test must assert or expect |
| Overmocking |
Mocking everything tests nothing real |
Only mock external boundaries |
| Copy-paste tests |
Duplicated tests diverge and rot |
Use parameterized tests or helpers |
| Testing the framework |
Verifying library code works |
Test your logic, trust dependencies |
| Ignoring test failures |
skip, xit, @Disabled accumulate |
Fix or delete — never hoard skipped tests |
| Tight coupling to DB |
Tests fail when schema changes |
Use repository pattern + fakes for unit tests |
| One giant test |
Single test covers 10 scenarios |
Split into focused, named tests |
| No test for bug fix |
Regression reappears later |
Every bug fix gets a regression test |
NEVER Do
- NEVER test implementation details instead of behavior — tests must verify what the code does, not how it does it
- NEVER use
sleep() in tests — use explicit waits, polling, events, or assertions that auto-retry
- NEVER share mutable state between tests — each test sets up and tears down its own state
- NEVER write assert-free tests — a test that asserts nothing proves nothing
- NEVER mock internal domain logic — only mock at system boundaries (network, DB, filesystem, clock)
- NEVER skip tests without a linked issue and a plan to re-enable — skipped tests rot into permanent gaps
- NEVER leave a test suite in a failing state — fix it or remove it with justification before moving on
- NEVER chase 100% coverage as a goal — coverage percentage is a tool, not a target; strong assertions on critical paths beat weak assertions everywhere
Summary
| Do |
Don't |
| Test behavior, not implementation |
Mock everything in sight |
| Write the test before fixing a bug |
Skip tests to ship faster |
| Keep tests fast and deterministic |
Use sleep() or shared state |
| Use factories for test data |
Copy-paste setup across tests |
| Mock at system boundaries |
Mock internal functions |
| Name tests descriptively |
Name tests test1, test2 |
| Run tests in CI on every push |
Only run tests locally |
| Delete or fix skipped tests |
Let @skip accumulate forever |
| Use parameterized tests for variants |
Duplicate test code |
| Inject dependencies for testability |
Hard-code dependencies |
Remember: Tests are a safety net — a fast, trustworthy suite lets you refactor fearlessly and ship with confidence.
1---2name: testing-patterns3description: Unit, integration, and E2E testing patterns with framework-specific guidance. Use when asked to "write tests", "add test coverage", "testing strategy", "test this function", "create test suite", "fix flaky tests", or "improve test quality".4---56# Testing Patterns78> **Write tests that catch bugs, not tests that pass.** — Confidence through coverage, speed through isolation.910---1112## Testing Pyramid1314| Level | Ratio | Speed | Cost | Confidence | Scope |15|-------|-------|-------|------|------------|-------|16| **Unit** | ~70% | ms | Low | Low (isolated) | Single function/class |17| **Integration** | ~20% | seconds | Medium | Medium | Module boundaries, APIs, DB |18| **E2E** | ~10% | minutes | High | High (realistic) | Full user workflows |1920> **Rule:** If your E2E tests outnumber your unit tests, invert the pyramid.2122---2324## Unit Testing Patterns2526### Core Patterns2728| Pattern | When to Use | Structure |29|---------|------------|-----------|30| **Arrange-Act-Assert** | Default for all unit tests | Setup, Execute, Verify |31| **Given-When-Then** | BDD-style, behavior-focused | Precondition, Action, Outcome |32| **Parameterized** | Same logic, multiple inputs | Data-driven test cases |33| **Snapshot** | UI components, serialized output | Compare against saved baseline |34| **Property-Based** | Mathematical invariants | Generate random inputs, assert properties |3536### Arrange-Act-Assert (AAA)3738The default structure for every unit test. Clear separation of setup, execution, and verification makes tests readable and maintainable.3940```typescript41// Clean AAA structure42test('calculates order total with tax', () => {43 // Arrange44 const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }];45 const taxRate = 0.08;4647 // Act48 const total = calculateTotal(items, taxRate);4950 // Assert51 expect(total).toBe(27.0);52});53```5455### Test Doubles5657Use the right type of test double for the situation. Each serves a different purpose.5859| Double | Purpose | When to Use | Example |60|--------|---------|-------------|---------|61| **Stub** | Returns canned data | Control indirect input | `jest.fn().mockReturnValue(42)` |62| **Mock** | Verifies interactions | Assert something was called | `expect(mock).toHaveBeenCalledWith('arg')` |63| **Spy** | Wraps real implementation | Observe without replacing | `jest.spyOn(service, 'save')` |64| **Fake** | Working simplified impl | Need realistic behavior | In-memory database, fake HTTP server |6566```typescript67// Stub — control indirect input68const getUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' });6970// Spy — observe without replacing71const spy = jest.spyOn(logger, 'warn');72processInvalidInput(data);73expect(spy).toHaveBeenCalledWith('Invalid input received');7475// Fake — lightweight substitute76class FakeUserRepo implements UserRepository {77 private users = new Map<string, User>();78 async save(user: User) { this.users.set(user.id, user); }79 async findById(id: string) { return this.users.get(id) ?? null; }80}81```8283### Parameterized Tests8485Use parameterized tests when the same logic needs verification with multiple inputs. This eliminates copy-paste tests while providing comprehensive coverage.8687```typescript88// Vitest/Jest89test.each([90 ['hello', 'HELLO'],91 ['world', 'WORLD'],92 ['', ''],93 ['123abc', '123ABC'],94])('toUpperCase(%s) returns %s', (input, expected) => {95 expect(input.toUpperCase()).toBe(expected);96});97```9899```python100# pytest101@pytest.mark.parametrize("input,expected", [102 ("hello", "HELLO"),103 ("world", "WORLD"),104 ("", ""),105])106def test_to_upper(input, expected):107 assert input.upper() == expected108```109110```go111// Go — table-driven tests (idiomatic)112func TestAdd(t *testing.T) {113 tests := []struct {114 name string115 a, b int116 expected int117 }{118 {"positive", 2, 3, 5},119 {"zero", 0, 0, 0},120 {"negative", -1, -2, -3},121 }122 for _, tc := range tests {123 t.Run(tc.name, func(t *testing.T) {124 if got := Add(tc.a, tc.b); got != tc.expected {125 t.Errorf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.expected)126 }127 })128 }129}130```131132---133134## Integration Testing Patterns135136### Database Testing Strategies137138| Strategy | Approach | Trade-off |139|----------|----------|-----------|140| **Transaction rollback** | Wrap each test in a transaction, rollback after | Fast, but hides commit bugs |141| **Fixtures/seeds** | Load known data before suite | Predictable, but brittle if schema changes |142| **Factory functions** | Generate data programmatically | Flexible, but more setup code |143| **Testcontainers** | Spin up real DB in Docker | Realistic, but slower startup |144145```typescript146// Transaction rollback pattern (Prisma)147beforeEach(async () => {148 await prisma.$executeRaw`BEGIN`;149});150afterEach(async () => {151 await prisma.$executeRaw`ROLLBACK`;152});153154test('creates user in database', async () => {155 const user = await createUser({ name: 'Alice', email: 'a@b.com' });156 const found = await prisma.user.findUnique({ where: { id: user.id } });157 expect(found?.name).toBe('Alice');158});159```160161### API Testing162163```typescript164// Supertest (Node.js)165import request from 'supertest';166import { app } from '../src/app';167168describe('POST /api/users', () => {169 it('creates a user and returns 201', async () => {170 const res = await request(app)171 .post('/api/users')172 .send({ name: 'Alice', email: 'alice@test.com' })173 .expect(201);174175 expect(res.body).toMatchObject({176 id: expect.any(String),177 name: 'Alice',178 });179 });180181 it('returns 400 for invalid email', async () => {182 await request(app)183 .post('/api/users')184 .send({ name: 'Alice', email: 'not-an-email' })185 .expect(400);186 });187});188```189190---191192## Mocking Best Practices193194### Mock Boundaries, Not Implementations195196The fundamental rule: mock at system boundaries (external APIs, databases, file systems) and never mock internal domain logic.197198```typescript199// BAD — mocking internal implementation200jest.mock('./utils/formatDate'); // Breaks on refactor201202// GOOD — mocking external boundary203jest.mock('./services/paymentGateway'); // Third-party API is the boundary204```205206### When to Mock vs Not Mock207208| Mock | Don't Mock |209|------|-----------|210| HTTP APIs, external services | Pure functions |211| Database (in unit tests) | Your own domain logic |212| File system, network | Data transformations |213| Time/Date (`Date.now`) | Simple calculations |214| Environment variables | Internal class methods |215216### Dependency Injection for Testability217218Structure code so dependencies can be swapped in tests. This is the single most impactful pattern for testable code.219220```typescript221// Injectable dependencies — easy to test222class OrderService {223 constructor(224 private paymentGateway: PaymentGateway,225 private inventory: InventoryService,226 private notifier: NotificationService,227 ) {}228229 async placeOrder(order: Order): Promise<OrderResult> {230 const stock = await this.inventory.check(order.items);231 if (!stock.available) return { status: 'out_of_stock' };232233 const payment = await this.paymentGateway.charge(order.total);234 if (!payment.success) return { status: 'payment_failed' };235236 await this.notifier.send(order.userId, 'Order confirmed');237 return { status: 'confirmed', id: payment.transactionId };238 }239}240241// In tests — inject fakes242const service = new OrderService(243 new FakePaymentGateway(),244 new FakeInventory({ available: true }),245 new FakeNotifier(),246);247```248249---250251## Framework Quick Reference252253| Framework | Language | Type | Test Runner | Assertion |254|-----------|----------|------|-------------|-----------|255| **Jest** | JS/TS | Unit/Integration | Built-in | `expect()` |256| **Vitest** | JS/TS | Unit/Integration | Vite-native | `expect()` (Jest-compatible) |257| **Playwright** | JS/TS/Python | E2E | Built-in | `expect()` / locators |258| **Cypress** | JS/TS | E2E | Built-in | `cy.should()` |259| **pytest** | Python | Unit/Integration | Built-in | `assert` |260| **Go testing** | Go | Unit/Integration | `go test` | `t.Error()` / testify |261| **Rust** | Rust | Unit/Integration | `cargo test` | `assert!()` / `assert_eq!()` |262| **JUnit 5** | Java/Kotlin | Unit/Integration | Built-in | `assertEquals()` |263| **RSpec** | Ruby | Unit/Integration | Built-in | `expect().to` |264| **PHPUnit** | PHP | Unit/Integration | Built-in | `$this->assert*()` |265| **xUnit** | C# | Unit/Integration | Built-in | `Assert.Equal()` |266267### Framework Command Quick Reference268269Use these commands as the default starting point before adding project-specific flags.270271| Framework | Install | Common Commands |272|-----------|---------|-----------------|273| **Vitest** | `npm install -D vitest @testing-library/react @testing-library/jest-dom` | `npx vitest`, `npx vitest run`, `npx vitest --coverage` |274| **Jest** | `npm install -D jest @types/jest ts-jest` | `npx jest`, `npx jest --watch`, `npx jest --coverage`, `npx jest path/to/test` |275| **pytest** | `uv pip install pytest pytest-cov pytest-asyncio httpx` | `pytest`, `pytest -v`, `pytest -x`, `pytest --cov=app`, `pytest tests/test_api.py -k "test_login"` |276| **XCTest** | Built into SwiftPM/Xcode | `swift test`, `swift test --filter MyTests`, `swift test --parallel` |277| **Playwright** | `npm install -D @playwright/test && npx playwright install` | `npx playwright test`, `npx playwright test --headed`, `npx playwright test --debug`, `npx playwright show-report` |278279---280281## Test Quality Checklist282283| Quality | Rule | Why |284|---------|------|-----|285| **Deterministic** | Same input produces same result, every time | Flaky tests erode trust |286| **Isolated** | No shared mutable state between tests | Order-dependent tests break in CI |287| **Fast** | Unit: < 10ms, Integration: < 1s, E2E: < 30s | Slow tests don't get run |288| **Readable** | Test name describes the scenario and expectation | Tests are documentation |289| **Maintainable** | Change one behavior, change one test | Brittle tests slow development |290| **Focused** | One logical assertion per test | Failures pinpoint the problem |291292> **Naming convention:** `test_[unit]_[scenario]_[expected result]` or `should [do X] when [condition Y]`293294---295296## Coverage Strategy297298### When to Aim for What299300| Target | When | Rationale |301|--------|------|-----------|302| **80%+ line coverage** | Business logic, utilities, core domain | High ROI — catches most regressions |303| **90%+ branch coverage** | Payment processing, auth, security-critical | Edge cases matter here |304| **100% coverage** | Almost never — diminishing returns | Getter/setter tests add noise, not confidence |305| **Mutation testing** | Critical paths after coverage is high | Verifies tests actually catch bugs |306307### What NOT to Test308309| Skip | Reason |310|------|--------|311| Generated code (Prisma client, protobuf) | Maintained by tooling |312| Third-party library internals | Not your responsibility |313| Simple getters/setters | No logic to verify |314| Configuration files | Test the behavior they configure instead |315| Console.log / print statements | Side effects with no business value |316317---318319## Test Organization320321```322src/323├── services/324│ ├── order.service.ts325│ └── order.service.test.ts # Co-located unit tests326├── api/327│ └── routes/328│ └── orders.ts329tests/330├── integration/331│ ├── api/332│ │ └── orders.test.ts # API integration tests333│ └── db/334│ └── order.repo.test.ts # DB integration tests335├── e2e/336│ ├── pages/ # Page objects337│ │ └── checkout.page.ts338│ └── specs/339│ └── checkout.spec.ts # E2E specs340└── helpers/341 ├── factories.ts # Test data factories342 └── setup.ts # Global test setup343```344345> **Rule:** Co-locate unit tests with source. Separate integration and E2E tests into dedicated directories.346347---348349## Anti-Patterns350351| Anti-Pattern | Problem | Fix |352|--------------|---------|-----|353| **Testing implementation** | Tests break on refactor, not on bugs | Test behavior and outputs, not internals |354| **Flaky tests** | Non-deterministic failures erode CI trust | Remove time/order/network dependencies |355| **Test pollution** | Shared mutable state leaks between tests | Reset state in `beforeEach` / `setUp` |356| **Sleeping in tests** | `sleep(2000)` is slow and unreliable | Use explicit waits, polling, or events |357| **Giant arrange** | 50 lines of setup obscure intent | Extract factories/builders/fixtures |358| **Assert-free tests** | Test runs but verifies nothing | Every test must assert or expect |359| **Overmocking** | Mocking everything tests nothing real | Only mock external boundaries |360| **Copy-paste tests** | Duplicated tests diverge and rot | Use parameterized tests or helpers |361| **Testing the framework** | Verifying library code works | Test *your* logic, trust dependencies |362| **Ignoring test failures** | `skip`, `xit`, `@Disabled` accumulate | Fix or delete — never hoard skipped tests |363| **Tight coupling to DB** | Tests fail when schema changes | Use repository pattern + fakes for unit tests |364| **One giant test** | Single test covers 10 scenarios | Split into focused, named tests |365| **No test for bug fix** | Regression reappears later | Every bug fix gets a regression test |366367---368369## NEVER Do3703711. **NEVER test implementation details instead of behavior** — tests must verify what the code does, not how it does it3722. **NEVER use `sleep()` in tests** — use explicit waits, polling, events, or assertions that auto-retry3733. **NEVER share mutable state between tests** — each test sets up and tears down its own state3744. **NEVER write assert-free tests** — a test that asserts nothing proves nothing3755. **NEVER mock internal domain logic** — only mock at system boundaries (network, DB, filesystem, clock)3766. **NEVER skip tests without a linked issue and a plan to re-enable** — skipped tests rot into permanent gaps3777. **NEVER leave a test suite in a failing state** — fix it or remove it with justification before moving on3788. **NEVER chase 100% coverage as a goal** — coverage percentage is a tool, not a target; strong assertions on critical paths beat weak assertions everywhere379380---381382## Summary383384| Do | Don't |385|----|-------|386| Test behavior, not implementation | Mock everything in sight |387| Write the test before fixing a bug | Skip tests to ship faster |388| Keep tests fast and deterministic | Use `sleep()` or shared state |389| Use factories for test data | Copy-paste setup across tests |390| Mock at system boundaries | Mock internal functions |391| Name tests descriptively | Name tests `test1`, `test2` |392| Run tests in CI on every push | Only run tests locally |393| Delete or fix skipped tests | Let `@skip` accumulate forever |394| Use parameterized tests for variants | Duplicate test code |395| Inject dependencies for testability | Hard-code dependencies |396397> **Remember:** Tests are a safety net — a fast, trustworthy suite lets you refactor fearlessly and ship with confidence.