PresentationInteractive, narrated presentation of the section content.

ContentDetailed description of the section content.

Diving Deeper into Unit Testing vs. Integration and End-to-End Testing

You are building our blogging app with Next.js, TypeScript, and React. You’ve got a function to format dates, a component to display posts, and an API to fetch them. You write a test, run it, and it passes—great! But then someone asks, “Is that a unit test, an integration test, or an end-to-end test?” Suddenly, you’re not so sure. Understanding the differences is key to testing effectively, saving time, and catching bugs where they matter most. Today, we’ll break it down using our blogging app, so you’ll know exactly what you’re testing and why it matters—especially in a CI/CD workflow.

You might think, “A test is a test, right? If it works, who cares what kind it is?” Not quite! Each type has a unique purpose, and mixing them up can lead to slow, confusing, or incomplete tests. Let’s explore this step-by-step with our app as the example.

Section 1: Unit Testing – Zooming In on the Smallest Pieces

Unit testing focuses on testing one thing at a time in complete isolation—like a single function or a React component. It’s like checking if a single Lego brick is the right shape before building a castle.

How It Works

For our blogging app, let’s test the formatDate function:

// utils/date.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
}

// tests/date.test.ts
import { describe, it, expect } from "vitest";
import { formatDate } from "../utils/date";

describe("formatDate", () => {
  it("formats a valid date correctly", () => {
    const date = new Date("2025-04-07");
    expect(formatDate(date)).toBe("April 7, 2025");
  });
  it("handles edge cases like invalid dates", () => {
    const invalidDate = new Date("invalid");
    expect(formatDate(invalidDate)).toBe("Invalid Date");
  });
});
  • What’s Happening: We’re testing just formatDate, nothing else. No API calls, no components—just the function.
  • Key Trait: Isolation. We don’t care how it’s used elsewhere; we just want it to work as expected.

Now, a React component example with React Testing Library:

// components/PostPreview.tsx
import { formatDate } from "../utils/date";

interface PostPreviewProps { title: string; date: Date; }
export function PostPreview({ title, date }: PostPreviewProps) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{formatDate(date)}</p>
    </div>
  );
}

// tests/PostPreview.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { PostPreview } from "../components/PostPreview";

describe("PostPreview", () => {
  it("renders title and formatted date", () => {
    render(<PostPreview title="My First Post" date={new Date("2025-04-07")} />);
    expect(screen.getByText("My First Post")).toBeDefined();
    expect(screen.getByText("April 7, 2025")).toBeDefined();
  });
});
  • What’s Happening: We’re testing the PostPreview component alone, passing it props directly. No fetching data, no parent components—just the component itself.

Characteristics of Unit Tests

  • Scope: One function, one component, one “unit.”
  • Speed: Super fast—runs in milliseconds because there’s no network or database.
  • Dependencies: Mocked or avoided (e.g., we don’t call a real API).
  • Goal: Verify the unit’s logic or rendering works correctly.

How to Know It’s a Unit Test

Ask yourself:

  • Am I testing just one thing (e.g., a function or component)?
  • Am I faking external stuff (like APIs or state) instead of using the real thing?
  • Does it run crazy fast without setup? If yes, it’s a unit test!

Section 2: Integration Testing – Checking the Connections

Integration testing steps up a level, checking how multiple units work together. It’s like testing if a few Lego bricks snap together properly before building the whole castle.

How It Works

Let’s test if PostPreview works with a mocked API fetch in a PostList component:

// components/PostList.tsx
import { useEffect, useState } from "react";
import { PostPreview } from "./PostPreview";

export function PostList() {
  const [posts, setPosts] = useState<{ title: string; date: Date }[]>([]);
  useEffect(() => {
    fetch("/api/posts")
      .then((res) => res.json())
      .then((data) => setPosts(data.map((p: any) => ({ ...p, date: new Date(p.date) }))));
  }, []);
  return (
    <div>
      {posts.map((post) => (
        <PostPreview key={post.title} title={post.title} date={post.date} />
      ))}
    </div>
  );
}

// tests/PostList.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { PostList } from "../components/PostList";

describe("PostList", () => {
  it("fetches and renders posts with PostPreview", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      json: () => Promise.resolve([{ title: "Test Post", date: "2025-04-07" }]),
    } as any);
    render(<PostList />);
    expect(await screen.findByText("Test Post")).toBeDefined();
    expect(await screen.findByText("April 7, 2025")).toBeDefined();
  });
});
  • What’s Happening: We’re testing PostList (which fetches data) and PostPreview (which displays it) together. The API is mocked, but the test checks their interaction.
  • Key Trait: Multiple pieces (fetching + rendering) are tested as a unit.

Characteristics of Integration Tests

  • Scope: A few components or functions working together (e.g., data fetching + UI).
  • Speed: Slower than unit tests—might involve async operations, even if mocked.
  • Dependencies: Some real interactions (e.g., component rendering), some mocked (e.g., API).
  • Goal: Ensure pieces integrate correctly—like data flowing from an API to the UI.

How to Know It’s an Integration Test

Ask yourself:

  • Am I testing more than one thing working together (e.g., a fetch and a component)?
  • Am I mocking some external parts (like an API) but still using real internal logic?
  • Does it take a bit longer than a unit test because of setup or async code? If yes, it’s an integration test!

Section 3: End-to-End (E2E) Testing – The Full Journey

E2E testing is the big picture—it’s like testing the entire Lego castle to see if it stands up. For our blogging app, we’ll use Playwright to simulate a user browsing posts and an admin adding one.

How It Works

// tests/e2e/blog.spec.ts
import { test, expect } from "@playwright/test";

test("user views posts and admin adds a post", async ({ page }) => {
  await page.goto("http://localhost:3000");
  await expect(page.getByText("My First Post")).toBeVisible();
  await expect(page.getByText("April 7, 2025")).toBeVisible();

  await page.goto("http://localhost:3000/admin");
  await page.fill("#title", "New Post");
  await page.fill("#date", "2025-04-08");
  await page.click("button[type='submit']");
  await page.goto("http://localhost:3000");
  await expect(page.getByText("New Post")).toBeVisible();
});
  • What’s Happening: We’re testing the full app—front-end, back-end, database, everything—as a real user would experience it.
  • Key Trait: No mocking; it’s all real, from browser clicks to server responses.

Characteristics of E2E Tests

  • Scope: The entire app, from UI to server to database.
  • Speed: Slow—takes seconds or minutes because it’s a full simulation.
  • Dependencies: All real—no mocks, just the live system.
  • Goal: Confirm the app works as a whole for real users.

How to Know It’s an E2E Test

Ask yourself:

  • Am I testing the whole app, like a user would use it?
  • Am I using a real browser and hitting real endpoints (no mocks)?
  • Does it take a while to run because it’s doing everything? If yes, it’s an E2E test!

Section 4: Spotting the Difference – Unit vs. Integration vs. E2E

Let’s compare them using our blogging app example:

  • Unit Test: Tests formatDate alone or PostPreview with fake props. Fast, isolated, no network.
  • Integration Test: Tests PostList fetching mock data and passing it to PostPreview. Checks interaction, still mocks external systems.
  • E2E Test: Tests the full app—user clicks, real API calls, database updates. Slow, comprehensive, no mocks.

Common Mix-Up Scenarios

  • You Think It’s a Unit Test, But It’s Integration:
    • If your “unit” test calls fetch or a database, it’s integration. Unit tests don’t touch external systems—they mock them.
    • Example: Testing PostPreview by fetching real data instead of passing props.
  • You Think It’s Integration, But It’s E2E:
    • If you’re using a browser (like Playwright) and hitting a live server, it’s E2E, not integration. Integration tests still control the environment (e.g., mock APIs).

Practical Tips to Tell Them Apart

  1. Look at Dependencies:
    • Unit: No real dependencies, all mocked or avoided.
    • Integration: Some real (e.g., component rendering), some mocked (e.g., API).
    • E2E: All real, no mocks.
  2. Check the Scope:
    • Unit: One thing (function/component).
    • Integration: A few things (fetch + render).
    • E2E: Everything (UI + server + DB).
  3. Measure Speed:
    • Unit: Milliseconds.
    • Integration: Hundreds of milliseconds.
    • E2E: Seconds or more.

Section 5: Testing in CI/CD – Where They Fit

In our CI/CD pipeline:

  • Unit Tests: Run on every commit—super fast, catch logic errors early (e.g., formatDate fails).
  • Integration Tests: Run on pull requests—verify system parts work together (e.g., API-to-UI flow).
  • E2E Tests: Run before deployment—ensure the app is production-ready (e.g., users can post).

This layering builds confidence step-by-step without slowing down development.

Conclusion

Unit, integration, and E2E tests each have a job in our blogging app:

  • Unit: Tests formatDate or PostPreview in isolation—fast and focused.
  • Integration: Tests PostList with a mock API—checks connections.
  • E2E: Tests the full app with Playwright—mimics real users.

How to Know What You Wrote:

  • Unit: One piece, no real external calls, lightning fast.
  • Integration: Multiple pieces, some mocks, a bit slower.
  • E2E: Full app, all real, takes time.

Key Takeaways:

  • Use unit tests for logic, integration for interactions, E2E for user flows.
  • Keep them separate—don’t let a unit test sneak in a fetch!
  • Match tests to your goal: speed (unit), cohesion (integration), or realism (E2E).

With this clarity, you’ll write the right test for the right job, making our blogging app rock-solid!

No hints available.

Discuss with OthersAsk questions, share your thoughts, and discuss with other learners.

Loading discussion ...