← Back to BlogPaul Morris
21 August 2025·By Paul Morris

Understanding Different Test Types

Unit, integration, and end-to-end tests all serve different purposes. The key is knowing what each is for, where it adds value, and who should own it.

One of the most common testing I've come across in different organisations is not a lack of effort, but a lack of clarity. Teams know they should be testing, but they are less clear on which types of tests they need, what those tests are supposed to prove, and who should be responsible for them.

I believe a good test strategy is not about having more tests. It is about using the right kinds of tests in the right places for the right reasons.
Paul Morris

At a high level, most modern software teams are working with a mix of unit tests, integration tests, and end-to-end tests. Each serves a different purpose.

Unit tests

Unit tests focus on the smallest pieces of behaviour in isolation. That might be a function, a class, a component, or a small section of business logic.

Their job is to tell you whether that one piece of logic behaves as expected under different inputs and conditions.

In most teams, unit tests are primarily owned by engineers because they sit closest to the implementation. They are usually fast, cheap to run, and helpful for catching regressions early.

This kind of test is typically checking a single piece of logic without involving the network, a real database, or the browser.

Unit test examplets
import { describe, expect, it } from "vitest";
import { calculateDiscount } from "./pricing";

describe("calculateDiscount", () => {
it("applies a 10% discount for premium customers", () => {
  const result = calculateDiscount(100, "premium");

  expect(result).toBe(90);
});
});

Integration tests

Integration tests sit in the middle. They check that different parts of the system work together correctly, such as a service talking to a database, an API layer calling another dependency, or a UI flow interacting with backend responses.

These tests are valuable because many real defects do not live inside a single isolated function. They happen where systems meet.

Ownership here is often shared. Engineers usually build them, but QA and quality-minded engineers often help shape where they provide the most value, especially when thinking about risk and coverage gaps.

An integration test might check that an API endpoint can call a real service layer and return the expected response shape.

Integration test examplets
import request from "supertest";
import { app } from "../app";

describe("POST /api/orders", () => {
it("creates an order and returns a 201 response", async () => {
  const response = await request(app)
    .post("/api/orders")
    .send({
      customerId: "cust_123",
      items: [{ sku: "ABC-123", quantity: 1 }],
    });

  expect(response.status).toBe(201);
  expect(response.body.orderId).toBeDefined();
});
});

End-to-end tests

End-to-end tests validate the system from the user’s point of view. They are the closest thing to exercising a real workflow through the application, often across UI, API, authentication, and persistence layers.

They are useful for proving that critical journeys work, but they are also slower, more expensive, and usually more fragile than lower-level tests.

That is why I do not see end-to-end tests as the foundation of a testing strategy. They are best used selectively on the paths that give the most confidence.

An end-to-end test is usually best used for the flows that really matter, such as signing in, completing a purchase, or submitting an important form.

End-to-end test examplets
import { expect, test } from "@playwright/test";

test("user can sign in and view the dashboard", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("paul@example.com");
await page.getByLabel("Password").fill("super-secure-password");
await page.getByRole("button", { name: "Sign in" }).click();

await expect(page).toHaveURL(/dashboard/);
await expect(
  page.getByRole("heading", { name: "Dashboard" })
).toBeVisible();
});
  • Use unit tests for logic and behaviour in isolation
  • Use integration tests where systems and boundaries meet
  • Use end-to-end tests to protect the journeys that matter most

Who is responsible?

This is where teams often get stuck. In healthy teams, quality is shared, but responsibility still needs to be clear.

Engineers should usually own the tests closest to the code, especially unit and integration coverage. QA professionals add the most value when they help shape strategy, identify gaps, focus attention on risk, and make sure the overall test approach reflects real user and business concerns.

In other words, QA should not be there only to “do the testing”, and engineers should not treat quality as something that happens later.

If a team understands what each test type is for, the conversations get much easier. You stop arguing about whether you need more tests and start asking a much better question: what kind of confidence do we actually need here?