Types of Testing

Testing is how we build confidence that software does what it should — and keeps doing it as the code changes. There is no single "test"; instead there is a spectrum of test types, each scoped to a different slice of the system and each catching a different class of defect. The skill is knowing which type answers which question, and investing in the right mix rather than testing everything the same way.

Why We Test

The later a defect is found, the more expensive it is to fix. A mistake caught while writing a function costs seconds; the same mistake caught in production can cost an incident, lost data, and lost trust. Good tests shift defect discovery to the left — as close to the moment the code is written as possible. Beyond catching bugs, tests act as executable documentation, a safety net that makes refactoring possible, and a design pressure that pushes code toward smaller, more decoupled units.

Two Dimensions of Testing

Before listing types, it helps to separate two independent questions that people often conflate.

  1. Scope — how much of the system a test exercises. This runs from a single function (unit) up through several collaborating components (integration) to the whole system driven like a real user (end-to-end). This is the axis the testing pyramid describes.
  2. What is verifiedfunctional tests check that the system produces the correct behaviour ("does it do the right thing?"), while non-functional tests check qualities like speed, capacity, and resilience ("how well does it do it?"). Load and performance testing live here.

A single test occupies a point on both axes — you can have a functional unit test and a non-functional end-to-end load test. Keeping the two dimensions distinct stops the taxonomy from feeling like an arbitrary list.

The Testing Pyramid

The testing pyramid is a heuristic for how many tests of each scope to write. Tests at the bottom are cheap, fast, and numerous; tests at the top are slow, brittle, and expensive, so you want fewer of them.

  1. Unit (base) — many small, fast tests around individual functions.
  2. Integration (middle) — fewer tests across component boundaries.
  3. End-to-end (top) — a small number of full-system journeys.

The common anti-pattern is the ice-cream cone: mostly slow end-to-end tests and few unit tests. It produces a suite that is slow to run, flaky, and painful to debug — a failing E2E test tells you something broke but not where. Push logic and coverage down the pyramid wherever you can.

Unit Testing

A unit test verifies the smallest testable piece of code — typically a single function, method, or class — in isolation from its dependencies. External collaborators (databases, network, other services) are replaced with test doubles: stubs, mocks, and fakes that stand in for the real thing so the test stays fast and deterministic.

Good unit tests are fast (milliseconds), isolated (no shared state), and focused on one behaviour. A widely used structure is Arrange–Act–Assert: set up the inputs, invoke the code under test, then assert on the result.

Spec / BDD style. "Spec" tests are not a separate scope but a style of writing unit and integration tests. Behaviour-Driven Development frameworks — Jasmine, RSpec, Jest, Mocha — encourage describing behaviour in readable sentences (describe("a cart") → it("applies a discount when...")) often following Given–When–Then. The scope is still a unit or a small integration; only the phrasing and intent (specifying behaviour, not just asserting output) differ.

Catches: logic errors, edge cases, regressions in individual components. Misses: anything about how components fit together.

Integration Testing

Integration tests verify that separately developed units work correctly together. This is where real collaborators come back in — the interaction between your code and a database, a message queue, an external API, or several internal modules wired up as they run in production.

Because they touch real infrastructure, integration tests are slower than unit tests and need more setup. Tools like Testcontainers spin up throwaway databases and services in Docker so tests run against the real technology instead of a mock. A narrower variant, contract testing (e.g. Pact), verifies that two services agree on the shape of the messages they exchange without running both end to end.

Catches: wiring bugs, incorrect assumptions about a dependency, schema and serialization mismatches, configuration errors. Misses: problems that only appear across the entire user journey.

End-to-End Testing

End-to-end (E2E) tests exercise the fully deployed system the way a real user would — clicking through a UI or driving the public API — with every layer running for real. They validate complete workflows: sign up, add to cart, check out, receive confirmation.

Browser-driving tools like Cypress, Playwright, and Selenium automate the front end; API-level E2E suites hit real endpoints. E2E tests give the highest confidence that the system actually works, but they are the slowest and most brittle: a small UI change or a timing issue can cause flaky failures unrelated to real bugs. Keep them few and reserve them for the critical paths that matter most to the business.

A closely related idea is the smoke test — a tiny subset of E2E checks run right after deployment to confirm the system is up and the core flows work before anyone relies on it.

Non-Functional Testing

Everything above checks correctness. Non-functional testing checks the qualities that determine whether correct software is actually usable in production — performance, scalability, security, reliability, and usability. The system can be perfectly correct and still fail if it falls over under load or leaks data.

Load & Performance Testing

Performance testing is a family of non-functional tests that measure how the system behaves under various levels of demand. The distinction between them is what kind of pressureyou apply.

  1. Load testing — apply the expected production traffic and confirm response times and throughput stay within target. Answers "does it hold up on a normal busy day?"
  2. Stress testing — push past expected limits until the system breaks, to find the breaking point and check that it fails gracefully rather than catastrophically.
  3. Spike testing — apply a sudden, sharp surge of traffic (a flash sale, a viral moment) and see how the system copes with the jump and recovers afterwards.
  4. Soak (endurance) testing — sustain a moderate load for a long period to surface slow problems like memory leaks and resource exhaustion.
  5. Scalability testing — increase load gradually while adding resources to confirm the system scales the way you expect.

Tools include k6, JMeter, Gatling, and Locust. Because these tests need production-like infrastructure and generate a lot of traffic, they are usually run in a dedicated environment rather than on every commit.

Other Useful Types

  1. Acceptance testing — confirms the software meets business requirements, often signed off by the customer. User Acceptance Testing (UAT) is done by real users before release.
  2. Regression testing — re-running existing tests after a change to ensure nothing that worked before is now broken. Usually automated and run in CI.
  3. Smoke & sanity testing — quick shallow checks that a build is stable enough to justify deeper testing.
  4. Security testing — probes for vulnerabilities: penetration testing, dependency scanning, and static/dynamic analysis (SAST/DAST).
  5. Usability & accessibility testing — verifies the product is easy to use and works for people with disabilities (e.g. against WCAG).
  6. Snapshot testing — records a component's rendered output and flags any unexpected change on later runs; common in front-end suites.

How Much of Each

TypeScopeSpeedBest at catching
UnitSingle function / classVery fastLogic errors, edge cases
IntegrationComponents togetherModerateWiring & dependency bugs
End-to-endWhole systemSlowBroken user journeys
Load / performanceWhole system under demandSlow, dedicated envBottlenecks, capacity limits

The rule of thumb is the pyramid: lean heavily on fast unit tests, back them with a solid layer of integration tests, and add a thin, carefully chosen set of end-to-end tests for the paths that matter most. Run non-functional tests like load and security on a schedule against a realistic environment rather than on every commit. No suite catches everything — the goal is the most confidence per second of test time, not one hundred percent coverage of every type.