DEV Community

Sheikh Limon
Sheikh Limon

Posted on

TDD + React Testing Pipeline — A Simple Example for Beginners

Yesterday I shared how to set up a modern testing environment using Vitest + React Testing Library.
Today — here’s what that setup looks like in action.

Let’s walk through the classic Red → Green → Refactor TDD loop using a simple “Add Task” feature

🟥 Step 1: Write a Failing Test (Red)

Before writing any UI, you define what the user should be able to do.

In App.test.tsx:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import App from "./App";

test("user can add a new task", async () => {
  render(<App />);

  const input = screen.getByRole("textbox", { name: /add task/i });
  const button = screen.getByRole("button", { name: /add/i });

  await userEvent.type(input, "Learn TDD");
  await userEvent.click(button);

  // ✅ Expectation: the new task appears in the list
  expect(screen.getByText("Learn TDD")).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

🧠 What's happening:

  • You describe behavior, not implementation.
  • You query by what a user would see.
  • You expect the UI to update.

This test will fail at first — because you haven't written any logic to handle "adding a task."

That's Red in the Red → Green → Refactor cycle.

🟩 Step 2: Implement Just Enough Code (Green)

Now write the minimal code to make the test pass.

In App.tsx:

import React from "react";

function App() {
  const [tasks, setTasks] = React.useState<string[]>([]);
  const [taskName, setTaskName] = React.useState("");

  const onAddTask = () => {
    if (!taskName.trim()) return;
    setTasks([...tasks, taskName]);
    setTaskName("");
  };

  return (
    <div>
      <h1>Tasks</h1>
      <label htmlFor="task-input">Add Task:</label>
      <input
        id="task-input"
        value={taskName}
        onChange={(e) => setTaskName(e.target.value)}
      />
      <button onClick={onAddTask}>Add</button>

      <ul>
        {tasks.map((t, index) => (
          <li key={index}>{t}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

🧠 Why this works:

  • The test types into the textbox and clicks "Add."
  • The component updates its tasks state → React re-renders → the new task appears.
  • The test now passes.

That's Green.

🟦 Step 3: Refactor (Optional)

Now that it's passing, you can safely refactor without breaking behavior.

Example improvement:

type Task = { id: number; title: string; isCompleted: boolean };

const [tasks, setTasks] = React.useState<Task[]>([]);
Enter fullscreen mode Exit fullscreen mode

You can change the implementation details freely — your test still passes because it only checks user-visible output, not internal state.

That's Refactor.

🔁 TDD Cycle Recap

Phase What You Do What Happens
🟥 Red Write a test that fails Defines the expected behavior
🟩 Green Write minimal code to pass Implement only what's necessary
🟦 Refactor Improve your code Clean up without breaking tests

💡 Why This Matters

This TDD process ensures:

  • You never write code without a clear purpose.
  • You only build what's needed to make the test pass.
  • Your tests describe how the app behaves, not how it's implemented.
  • You gain confidence that refactoring won't break anything visible to users.

🧭 Summary

Concept Role
React Testing Library Lets you test UI from the user's perspective
Virtual DOM In-memory structure for rendering during tests
Queries (screen.getBy*) Mimic user interactions (click, type, etc.)
TDD Forces small, confident, behavior-driven development steps
jest-dom Makes assertions human-readable (toBeInTheDocument)

🗒️ Remember
When you test what users see (not what you write in state), your tests become stable, your code becomes safer to refactor, and you start thinking like a product developer, not just an engineer.

Top comments (0)