
Most teams have tests but their tests do not actually catch bugs. They test happy paths and ignore the failure cases that actually break production.
A testing strategy that catches bugs requires a different approach.
The wrong way to test
Testing implementation details. Testing that a specific helper function returns the right value. This test breaks when you refactor the code, even though the user-facing behavior does not change. These tests are maintenance burden.
Testing everything equally. Testing one line of critical business logic with the same effort as a CSS selector. You cannot test everything equally. You have to prioritise.
Chasing coverage percentage. “We have 80% test coverage.” This says nothing about whether tests catch real bugs. You can have 95% coverage and still have untested critical paths.
Testing without understanding failure modes. Tests that pass are good, but tests that fail and actually tell you what is wrong are valuable.
The right approach
Test the critical paths. What must not break in production? Authentication, payment processing, data persistence, core features. These paths get the most testing effort.
Test the failure cases. What happens when the database is down? When an API returns an error? When the network is slow? These are the scenarios that break production.
Test the boundaries. Off-by-one errors, empty lists, null values, maximum limits. Bugs hide at boundaries.
Test integration, not just units. A function works in isolation but breaks in the real system. Test the full flow.
Test what users see. If a user sees a page with data, test the entire flow from database to display. Test what matters to the user.
The testing pyramid
The most effective testing strategy uses different types of tests in proportion:
Unit tests (base of pyramid). Fast, specific, many of them. Test individual functions and classes. Easy to write.
Integration tests (middle). Slower, test multiple components together. Test that services talk to each other correctly.
End-to-end tests (top). Slowest, fewest of them. Test the full application from user perspective.
The pyramid says: lots of unit tests, fewer integration tests, very few end-to-end tests.
This is inverted at many companies, where end-to-end tests dominate and unit tests are sparse.
Prioritisation
For a typical web app, prioritise in this order:
1. Authentication and authorisation. If this breaks, everyone is locked out. 2. Data operations. Create, read, update, delete. If this breaks, data is lost or corrupted. 3. Critical workflows. The most important user journey. For a shop, that is browse → add to cart → checkout. 4. Error handling. What happens when something fails? 5. Performance. Does it load in reasonable time? 6. Edge cases. Empty states, null values, boundaries.
Everything else is lower priority.
Test categories that work
Smoke tests. The absolute minimum. Does the app start? Can you log in? Can you reach the main page? These catch catastrophic failures.
Critical path tests. The most important workflow. For an app, this is probably the most common user action. Test it thoroughly.
Regression tests. After you fix a bug, write a test that would have caught it. This prevents the same bug coming back.
Boundary tests. Zero, one, maximum. Empty, null, very large. These catch off-by-one errors.
Error case tests. API returns 500. Database is down. Network is slow. Network is offline. Permission denied. Test all of these.
What not to test
UI details. Testing that a button is exactly 32 pixels wide is not valuable. Test that the button works.
Minor business logic. A utility function that formats a phone number. Worth testing, but not worth elaborate test infrastructure.
Third-party code. If you use a library, it is already tested. Test that you are using it correctly, not that it works.
Configuration. Test that configuration is read correctly, not every configuration option.
Test quality matters more than coverage
A test that always passes is worthless. A test that sometimes fails for unclear reasons is worse than worthless. A test that fails and clearly tells you what is wrong is valuable.
A test that takes 5 seconds to run is usually not worth the time cost. Tests need to be fast or they won’t run.
A test that breaks whenever you refactor is not valuable. Tests should test behavior, not implementation.
The realistic strategy for a small team
1. Write tests for critical paths. The most important workflows get tested thoroughly.
2. Write tests after bugs. When a bug is found, write a test that would have caught it.
3. Test failure cases. What breaks? What shouldn’t break? Write tests for both.
4. Keep test suite fast. If tests take more than 5 minutes, the team will not run them.
5. Automate test running. Every commit runs tests. Developers see results immediately.
6. Do not chase coverage. If a piece of code is not critical, testing it is lower priority than testing critical paths.
This is not comprehensive. It does not get to 80% coverage. But it catches most real bugs.
The tools
JavaScript/TypeScript: Jest (unit tests), Cypress or Playwright (end-to-end tests)
Python: pytest (unit and integration tests), Selenium or Playwright (end-to-end tests)
Go: testing package (unit tests), testify for assertions
Most languages: unit testing is built in. Pick an end-to-end tool separately.
Do not get hung up on tool choice. All modern testing tools are adequate.
The impact
A team with a good testing strategy catches most bugs before they reach production. They ship faster because they are not spending time on manual testing. They feel confident that code works.
A team without testing strategies fixes bugs in production and spends time on manual testing.
The difference in pace is often 2x. A testing strategy is one of the highest-leverage investments a team can make.