Good tests are a leverage multiplier — they let you change code with confidence, ship faster, and catch regressions before users do. Bad tests are a tax — slow, flaky, brittle, expensive to maintain. The difference is mostly process and discipline, not tool choice. This guide is the practical playbook for writing tests that actually pay back.
What changed in 2026
- AI-generated tests became normal. Cursor, Claude, and Copilot can scaffold tests in seconds — but the auto-generated tests are often the wrong tests, testing implementation details rather than behavior.
- Snapshot testing fell out of favor for non-trivial cases. Inline assertions are clearer and harder to "just regenerate" thoughtlessly.
- Vitest overtook Jest in new projects by speed and ESM support — see Vitest vs Jest in 2026.
The pyramid still works
The testing pyramid (lots of unit tests, fewer integration tests, even fewer end-to-end) is still right in 2026 because it reflects test cost: unit tests are 10-100× faster and more deterministic than E2E. A typical good test suite for a web application:
- Unit tests (~70%): pure functions, business logic, individual components in isolation
- Integration tests (~25%): database interactions, API routes, multi-component workflows
- E2E tests (~5%): critical user paths only — sign up, checkout, the 3-5 paths your product cannot ship broken
Inverting this pyramid (lots of E2E, few unit tests) is how you end up with 30-minute test suites that nobody trusts.
The AAA pattern
Arrange, Act, Assert. Three blocks per test, separated visually:
test('subtotal includes tax', () => {
// Arrange
const cart = newCart([{ price: 100, qty: 2 }]);
// Act
const total = cart.totalWithTax(0.08);
// Assert
expect(total).toBe(216);
});
One concept per test. If the assert section has more than 1-3 expectations, the test is doing too much. Split it.
Common test smells
Testing implementation, not behavior. A test that breaks every time you refactor is testing the wrong thing. Test what the code does, not how.
Excessive mocking. If you mock 8 dependencies to test one function, the function has too many dependencies. Listen to the test.
Shared mutable state between tests. Tests run in parallel. Globals, singletons, and shared fixtures cause flake. Each test owns its setup.
Snapshot tests that nobody reads. Auto-generated snapshots get regenerated mindlessly. They prove the output didn't change, not that it's correct. Use sparingly.
Slow unit tests. A unit test that takes >50ms isn't a unit test. It's an integration test pretending. Speed up or move it.
Tests that pass when broken. If you don't have a "make this fail first" step in your TDD or test-writing flow, you'll write tests that pass for the wrong reason.
Coverage is necessary but not sufficient
| Coverage % |
What it means in practice |
| <50% |
Most paths untested; refactoring is dangerous |
| 50-70% |
Critical paths covered; many edge cases missed |
| 70-85% |
Sweet spot for most apps; diminishing returns above |
| 85-95% |
Good for libraries and shared code |
| 95-100% |
Often theater — covering trivial code, missing real behavior |
Coverage is necessary but not sufficient. 100% coverage with bad assertions is worse than 70% coverage with good ones. Ask: if I delete the code, do tests fail? If I introduce a bug, do tests catch it?
What to test first when adding tests to a legacy codebase
- Critical revenue paths. Checkout, signup, billing.
- Bug-prone areas. Look at git blame for recent fixes — those modules need regression tests.
- Public API surface. What other code depends on this? Test the contract.
- Pure functions. Easiest to test, biggest immediate ROI.
What to skip first: UI styling, glue code, simple getters/setters, anything that changes weekly for product reasons.
AI-generated tests in 2026
LLMs can scaffold tests in seconds. The good: covers the easy paths, generates the boilerplate, suggests edge cases you might miss. The bad: tends to test implementation rather than behavior, generates plausibly-named tests that don't actually assert what they claim, hallucinates API methods.
The pattern that works: you write the test name and the AAA structure; AI fills in the test body. That keeps you in the design seat and lets the model do the typing.
FAQ
Should I do TDD?
Sometimes. Pure TDD shines for well-understood domains (parsers, calculators, business rules). For exploratory UI work, writing tests after the design settles is fine.
E2E tests with Playwright vs Cypress in 2026?
Playwright now leads — faster, multi-browser by default, better debugging. Cypress is still solid but Playwright became the default for new projects in 2025-2026.
How long should tests take to run?
Unit tests: under 30 seconds for the suite. Integration: under 2 minutes. E2E: under 10 minutes for the critical-path subset. Anything longer is a productivity killer.
Should AI write tests for me?
Use it for boilerplate and scaffolding. Review every assertion. Never accept "tests pass" as evidence the code works without reading the tests.
Where to go next
For related coverage see Vitest vs Jest in 2026, TypeScript strict mode guide in 2026, and AI coding agents workflows in 2026.