PresentationInteractive, narrated presentation of the section content.

ContentDetailed description of the section content.

Integration Testing and Component Testing with Vitest and React Testing Library

Picture our blogging app built with Next.js, TypeScript, and React 19: users browse posts by tag, admins edit them, and everything’s humming along—until a component fails to load data or a button does nothing. Testing individual pieces isn’t enough; we need to ensure they work alone and together. Today, we’ll master component testing and integration testing with Vitest and React Testing Library, set them up in our project, and tackle React 19’s async components using our blogging app as the guinea pig.

You might think, “I’ll just eyeball it in the browser,” but that misses sneaky bugs—like an async fetch failing silently. Or maybe, “Setup sounds hard.” Surprise: it’s straightforward, and with Vitest’s speed and React Testing Library’s user-first approach, we’ll catch issues fast—spying and mocking included!

Section 1: Setting Up Vitest and React Testing Library

Before we test, let’s get our tools ready in our Next.js project. Vitest is our test runner, and React Testing Library helps us test components like users see them.

Step-by-Step Setup

  1. Install Dependencies
    • Run this in your Next.js project:

      npm install --save-dev vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdom
    • Why:
      • vitest: Fast test runner with TypeScript support.
      • @vitejs/plugin-react: Enables React in Vitest.
      • @testing-library/react: Renders and queries React components.
      • @testing-library/jest-dom: Adds handy assertions (e.g., toBeInTheDocument).
      • jsdom: Simulates a browser environment for tests.
  2. Configure Vitest
    • Create vitest.config.ts in the project root:

      import { defineConfig } from "vitest/config";
      import react from "@vitejs/plugin-react";
      
      export default defineConfig({
        plugins: [react()],
        test: {
          environment: "jsdom", // Simulates browser for React
          globals: true, // Allows `describe`, `it` without imports
          setupFiles: "./tests/setup.ts", // Custom setup file
        },
      });
    • Explanation: This tells Vitest to use React and jsdom, and enables global test functions for cleaner code.
  3. Setup File
    • Create tests/setup.ts:

      import "@testing-library/jest-dom";
    • Why: Extends Vitest with Jest-DOM matchers (e.g., toBeDefined becomes toBeInTheDocument).
  4. Update <strong>package.json</strong>
    • Add test scripts:

      "scripts": {
        "test": "vitest run",
        "test:watch": "vitest"
      }
    • Explanation: test runs all tests once; test:watch runs them interactively.
  5. TypeScript Config
    • Ensure tsconfig.json includes:

      {
        "compilerOptions": {
          "types": ["vitest/globals", "@testing-library/jest-dom"]
        }
      }
    • Why: Adds type definitions for Vitest and Jest-DOM.

Verify It Works

  • Create a dummy test file, tests/example.test.tsx:

    import { describe, it, expect } from "vitest";
    
    describe("Setup Check", () => {
      it("works", () => {
        expect(true).toBe(true);
      });
    });
  • Run npm test. If “1 passed” shows, you’re set!

Blogging App Context

  • Our app lives in pages/, components/, and utils/. Tests go in tests/ or next to files (e.g., components/PostPreview.test.tsx).

Section 2: Component Testing – Testing the Building Blocks

Component testing ensures individual React components work in isolation—like a single post preview in our blogging app.

Why It Matters

  • Misconception: “If it renders, it’s good.” Wrong! A component might look fine but break with real props or async data.
  • React 19’s async components (e.g., with Suspense) need special testing for loading and resolved states.

How It Works

Let’s test a PostPreview component.

Simple Example

// 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>
  );
}

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

// tests/PostPreview.test.tsx
import { render, screen } from "@testing-library/react";
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")).toBeInTheDocument();
    expect(screen.getByText("April 7, 2025")).toBeInTheDocument();
  });
});
  • Explanation: We pass props and check output with screen.getByText. Vitest runs this in jsdom, simulating a browser.
  • How It Works: Fast (milliseconds), no external calls—just pure component logic.

Advanced Example: Async Component with React 19

React 19 improves async rendering. Let’s test an async PostContent component:

// components/PostContent.tsx
import { Suspense } from "react";

interface PostContentProps { postId: string; }
async function fetchPostBody(postId: string): Promise<string> {
  const res = await fetch(`/api/post/${postId}`);
  return res.text();
}

export async function PostContent({ postId }: PostContentProps) {
  const body = await fetchPostBody(postId);
  return <p>{body}</p>;
}

// tests/PostContent.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { PostContent } from "../components/PostContent";
import { Suspense } from "react";

describe("PostContent", () => {
  it("renders fetched post body with Suspense", async () => {
    global.fetch = vi.fn().mockResolvedValue({ text: () => Promise.resolve("Hello, world!") } as any);
    render(
      <Suspense fallback={<p>Loading...</p>}>
        <PostContent postId="1" />
      </Suspense>
    );
    expect(screen.getByText("Loading...")).toBeInTheDocument();
    await waitFor(() => expect(screen.getByText("Hello, world!")).toBeInTheDocument());
  });
});
  • Explanation: We mock fetch with Vitest’s vi.fn(), test the loading state, and use waitFor for the resolved state.
  • How It Works: Suspense handles async rendering, a React 19 perk, and we verify both phases.

Pros and Cons

  • Pros: Quick, isolates UI bugs, perfect for React 19 async testing.
  • Cons: Misses interactions with other components or real APIs.

Section 3: Integration Testing – Connecting the Dots

Integration testing checks how components work together—like fetching posts and displaying them in our blogging app.

Surprise Factor

  • Misconception: “Component tests are enough.” Nope! A component might pass alone but fail when fetching data or updating state.
  • Async flows in React 19 (e.g., API calls) need integration tests to catch timing issues.

How It Works

Let’s test PostList with PostPreview.

Simple Integration Example

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

interface Post { title: string; date: Date; }
export function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);
  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 { PostList } from "../components/PostList";

describe("PostList", () => {
  it("fetches and renders multiple posts", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve([
        { title: "Post 1", date: "2025-04-07" },
        { title: "Post 2", date: "2025-04-08" },
      ]),
    } as any);
    render(<PostList />);
    expect(await screen.findByText("Post 1")).toBeInTheDocument();
    expect(await screen.findByText("April 7, 2025")).toBeInTheDocument();
    expect(await screen.findByText("Post 2")).toBeInTheDocument();
  });
});
  • Explanation: Tests fetching and rendering together. findByText waits for async updates.
  • How It Works: Slower than component tests, but ensures data flows to child components.

Advanced Example: Spying and Mocking with Filters

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

interface Post { title: string; date: Date; tag: string; }
export function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [tag, setTag] = useState<string | null>(null);

  useEffect(() => {
    const url = tag ? `/api/posts?tag=${tag}` : "/api/posts";
    fetch(url)
      .then((res) => res.json())
      .then((data) => setPosts(data.map((p: any) => ({ ...p, date: new Date(p.date) }))));
  }, [tag]);

  return (
    <div>
      <button onClick={() => setTag("tech")}>Filter Tech</button>
      {posts.map((post) => (
        <PostPreview key={post.title} title={post.title} date={post.date} />
      ))}
    </div>
  );
}

// tests/PostList.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { PostList } from "../components/PostList";

describe("PostList", () => {
  it("filters posts by tag when button clicked", async () => {
    const fetchSpy = vi.spyOn(global, "fetch")
      .mockResolvedValueOnce({
        json: () => Promise.resolve([{ title: "Post 1", date: "2025-04-07", tag: "life" }]),
      } as any)
      .mockResolvedValueOnce({
        json: () => Promise.resolve([{ title: "Tech Post", date: "2025-04-08", tag: "tech" }]),
      } as any);

    render(<PostList />);
    expect(await screen.findByText("Post 1")).toBeInTheDocument();

    fireEvent.click(screen.getByText("Filter Tech"));
    expect(await screen.findByText("Tech Post")).toBeInTheDocument();
    expect(screen.queryByText("Post 1")).toBeNull();
    expect(fetchSpy).toHaveBeenCalledWith("/api/posts?tag=tech");
  });
});
  • Spying: fetchSpy tracks calls and confirms the filtered URL.
  • Mocking: Two mock responses simulate initial and filtered fetches.
  • Explanation: Tests state updates, refetching, and re-rendering—true integration!

Pros and Cons

  • Pros: Catches interaction bugs, tests async flows.
  • Cons: Slower, more complex to debug than component tests.

Section 4: Spying and Mocking – Mastering Control

Spying and mocking let us fake behavior and inspect calls, crucial for integration tests.

Spying

  • What: Tracks function calls (e.g., fetchSpy checks URLs).
  • How: vi.spyOn(global, "fetch") monitors without altering behavior unless mocked.
  • Use: Verify interactions (e.g., button click triggers correct fetch).

Mocking

  • What: Fakes implementations.
  • Tools:
    • vi.fn(): Custom mock function.
    • vi.spyOn().mockResolvedValue(): Mocks async responses.
  • Example: Mocking fetch for controlled test data.

We'll cover spying and mocking in the next lecture!

Conclusion

With Vitest and React Testing Library set up, we’ve armed our blogging app with powerful testing:

  • Setup: Install deps, configure vitest.config.ts, and you’re ready to test in Next.js.
  • Component Testing: Tests PostPreview and async PostContent—fast, isolated, React 19-ready.
  • Integration Testing: Tests PostList with PostPreview—ensures flows work.
  • Spying/Mocking: Controls tests, verifies behavior.

Key Takeaways

  • Component tests for UI pieces, integration for interactions.
  • Use waitFor and Suspense for React 19 async components.
  • Spy with vi.spyOn, mock with vi.fn or mockResolvedValue.
  • Setup is simple—start testing today!

Best Practices

  • Test user-facing output, not internals.
  • Mock external APIs, not internal logic.
  • Use afterEach(() => vi.restoreAllMocks()) to reset mocks.

Pitfalls to Avoid

  • Don’t skip setup—types and jsdom are key.
  • Don’t over-test internals—focus on what users see.
  • Don’t forget async—React 19 demands it.

Our blogging app’s front-end is now testable and reliable—let’s keep building!

 

No hints available.

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

Loading discussion ...