Most teams have a version of this conversation: "Our test coverage is at 87%, but every deploy is stressful." The number is high, but the confidence is low. What's wrong?
Code coverage measures how many lines were executed during tests. It doesn't measure whether tests verify correct behavior, whether they cover the cases that matter, or whether a refactor that breaks the application will be detected. You can have 100% coverage and a test suite that tests nothing useful.
The testing pyramid revisited
The classic pyramid model, many unit tests, fewer integration tests, even fewer E2E, is still valid as a principle, but it gets misapplied in practice.
The most common mistake: writing hundreds of unit tests that test implementation, not behavior. When code is refactored, all the tests break, even if the external behavior didn't change. This creates strong negative feedback: refactoring = pain. Teams that go through this stop refactoring.
The correct principle, popularized by Kent C. Dodds: the more your tests resemble how the software is used, the more confidence they give.
Testing behavior, not implementation
The practical distinction:
// ❌ Tests implementation: fragile
it('should call setLoading(true) then fetch then setLoading(false)', () => {
const setLoading = jest.fn()
// ... tests internal details
})
// ✅ Tests behavior: reliable
it('shows spinner while loading, then displays results', async () => {
render(<ProductList />)
expect(screen.getByRole('progressbar')).toBeInTheDocument()
await screen.findByText('Product A')
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
expect(screen.getByText('Product A')).toBeInTheDocument()
})
The first test breaks on any implementation refactor. The second survives any refactor that preserves visible behavior.
The cases that matter to cover
Instead of chasing percentages, think in terms of coverage categories that matter:
Happy path
Does the main flow work? Does valid input produce expected output? This is the minimum, necessary but not sufficient.
Edge cases
What happens with empty input? With extreme values? With null where it's not expected? With very long strings? Most real bugs live here.
Error cases
What happens when the external API fails? When the database is unavailable? When the user lacks permission? If you don't test error paths, you don't know what your users will see when something goes wrong.
Business invariants
The rules that can never be violated: balance can't go negative, a user can't access another user's data, an order can't be approved without an item. These tests are executable documentation of business rules.
Integration tests: the ignored sweet spot
For most applications, integration tests are where the ROI on tests is highest. An integration test that exercises the real endpoint, with an in-memory or test database, covers more behavior with less code than dozens of isolated unit tests.
// Integration test with test database: high confidence
describe('POST /orders', () => {
it('creates order and deducts inventory', async () => {
await db.product.create({ id: 'p1', stock: 5 })
const res = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${userToken}`)
.send({ productId: 'p1', quantity: 2 })
expect(res.status).toBe(201)
expect(res.body.order.status).toBe('confirmed')
const product = await db.product.findUnique({ where: { id: 'p1' } })
expect(product.stock).toBe(3)
})
})
That single test validates: authentication works, input validation works, order logic works, inventory update works, response is correct. Five behaviors in one test.
The right metric to measure confidence
Instead of line coverage, measure:
- How many bugs went to production that the suite didn't detect in the last 3 months? If the answer is high, you have coverage gaps in important behaviors.
- How much time do you spend "fixing" tests that broke without behavior changing? If it's high, you have fragile tests that cost more than they deliver.
- Do you deploy confidently or with fingers crossed? The answer says more about test quality than any coverage number.
60% coverage with well-written tests gives more confidence than 90% coverage with tests that test implementation. Optimize for confidence, not for metric.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk