Testing
Types of Testing
Testing isn't one-size-fits-all. Different types of tests serve different purposes, catch different bugs, and come with different costs. Understanding the landscape helps you build a testing strategy that actually protects your application without slowing down development.
The Testing Pyramid
The testing pyramid is the most useful mental model for thinking about test strategy. At the base are many unit tests (fast, cheap, focused). In the middle are a moderate number of integration tests (medium speed, medium cost). At the top are few end-to-end tests (slow, expensive, high confidence). This shape reflects reality: you should write many tests that run in milliseconds, fewer tests that take seconds, and very few tests that take minutes.
A common mistake is inverting the pyramid: writing lots of slow end-to-end tests and few unit tests. This gives you slow feedback loops, high costs, and brittle test suites. Build from the bottom up.
Unit Testing
Unit tests exercise a single function or component in isolation, mocking all its dependencies. They run in milliseconds and catch logic errors early. A unit test doesn't care about the database, the API, or the network—it only tests one piece of code.
Unit tests are the foundation of your test suite. They're cheap to write, fast to run, and deterministic (they give the same result every time). They give you confidence to refactor and serve as documentation for how a function should behave.
Integration Testing
Integration tests verify that multiple units work correctly together. They test real interactions: calling an API endpoint that reads from the database, or a component that calls an API and displays the result. Integration tests use real databases, real HTTP calls (or mocked versions), and sometimes real external services.
Integration tests are slower than unit tests (seconds instead of milliseconds) but catch bugs that unit tests miss. They're essential for finding problems at the boundaries between systems. A common pattern is to test an API endpoint fully: check that it validates input, queries the database correctly, and returns the right response.
End-to-End Testing
End-to-end (E2E) tests simulate a real user navigating your application in a browser. They click buttons, fill forms, navigate pages, and check that the UI displays the right content. E2E tests run against the full stack—frontend, backend, database, and all external services—in an environment as close to production as possible.
E2E tests are the slowest and most expensive to write and maintain, but they give the highest confidence that your application works from a user's perspective. You shouldn't have many of them; focus on critical user journeys (signing up, making a purchase, submitting a form). Don't use E2E tests to verify every UI variant when a unit test would do.
Static Analysis and Linting
Static analysis tools like ESLint, TypeScript, and Prettier catch entire categories of bugs without running your code. A syntax error, type mismatch, or unused variable is caught before your tests even start. These tools are fast (milliseconds), cheap (often free), and prevent whole classes of issues.
Many teams underestimate static analysis. A good TypeScript configuration and a strict ESLint setup can eliminate many bugs that would otherwise need dynamic testing. Think of them as the base of the pyramid—faster and cheaper than unit tests.
Functional vs Non-Functional Testing
Functional tests verify that your application does what it's supposed to do. Does the login form accept valid credentials? Does the search return results? Most testing falls into this category.
Non-functional tests verify properties that aren't about what the app does, but how well it does it:
- Performance testing: Does it load in under 3 seconds? Can it handle 10,000 concurrent users?
- Security testing: Are credentials stored securely? Is the API protected from injection attacks?
- Accessibility testing: Can users with screen readers navigate your site?
- Reliability testing: Does it work without the internet connection?
- Usability testing: Can a real user figure out how to use it?
Regression Testing
Regression testing means re-running existing tests after a change to ensure you didn't break anything. Every test in your suite is a regression test in this sense—when you fix a bug, you write a test to verify it doesn't come back.
Automated test suites are specifically designed for regression testing. Run them on every commit, and you'll catch regressions immediately instead of discovering them in production.
Smoke Tests
A smoke test is a quick sanity check that the basic functionality works. It's the lightest form of testing—does the app start? Can you load the homepage? Can you log in? Smoke tests catch catastrophic failures (the app won't even load) and are usually the first thing that runs in a test pipeline.
Many teams run smoke tests against production after a deployment to make sure nothing is fundamentally broken. It's not a substitute for thorough testing, but it's a quick early warning system.
Acceptance Testing
Acceptance tests verify that your application meets the requirements. They often take the form of scenarios: "When I log in with valid credentials, I should see my dashboard." These can be written in plain English using tools like Gherkin/Cucumber, or they can be automated E2E tests.
The value of acceptance testing is in the collaboration: writing tests forces you to define "done" clearly. What does it mean for a feature to be complete? Acceptance tests answer that question precisely.
Testing in Different Environments
Your testing strategy should span multiple environments:
- Local: Developers run tests on their machines during development.
- CI (Continuous Integration): Tests run automatically on every commit to catch issues before they reach main.
- Staging: An environment that mirrors production where you can test before deploying.
- Production: The live environment where users access your app. Smoke tests and monitoring run here.
How Much Testing Is Enough?
There's no universal answer, but here's a framework: the cost of a bug increases exponentially based on when it's found. A bug caught by a unit test costs nearly nothing. A bug found by a user in production costs customer support time, reputation damage, and an emergency hotfix. Testing should prevent expensive bugs from reaching production.
For critical features (payment processing, authentication, data loss), invest in comprehensive testing. For less critical features, lighter testing is fine. Consider your risk tolerance and your users' expectations.
Code coverage (the percentage of code lines executed by tests) is a useful metric, but not a goal. Aim for coverage of critical paths and edge cases, not 100% coverage. High coverage with bad tests is worse than thoughtful selective testing.
The Cost of Testing vs Not Testing
Writing tests takes time upfront. But not testing costs more in the long run:
| Aspect | WithTesting | WithoutTesting |
|---|---|---|
| Development speed (short-term) | Slower—writing tests alongside code | Faster—ship features immediately |
| Development speed (long-term) | Faster—refactor safely, fewer surprises | Slower—bugs accumulate, changes break things |
| Confidence in changes | High—tests verify nothing broke | Low—hope for the best, ask QA to check |
| Bug discovery timing | Immediately in CI | Staging, production, or user reports |
| Maintenance difficulty | Easier—tests guide refactoring | Harder—changes cause mysterious bugs |
| Cost of a bug reaching production | Low—it shouldn't happen | High—emergency fixes, user impact |
Choosing Your Testing Strategy
Different projects need different testing approaches. A simple static website doesn't need the same testing infrastructure as a financial system. Consider:
- How critical is your application? (Personal blog vs healthcare platform?)
- How often do things change? (Frequent iterations need good tests for safety.)
- How many developers work on it? (More developers = more need for tests to prevent stepping on each other.)
- What's your testing budget? (Small startup vs enterprise have different resources.)
- What are your biggest risks? (Security? Performance? Data loss? Focus testing there.)
What's Next
Now that you understand the landscape, we'll dive into each type in detail. Start with unit testing—it's the foundation and the easiest to learn. From there, move to integration testing, end-to-end testing, and finally the broader testing practices like TDD and QA processes.