Skip to main content

Command Palette

Search for a command to run...

How I Learned Unit Testing with React Testing Library (RTL) by Implementing It in a Real Project

Updated
5 min read
How I Learned Unit Testing with React Testing Library (RTL) by Implementing It in a Real Project
S

I am Saurabh Gupta, a dedicated frontend developer with over 2.5 years of experience. My expertise lies in building user-friendly and responsive web applications using technologies like React and Next.js. I have a strong background in integrating APIs and working with modern JavaScript libraries. I am passionate about creating efficient, scalable, and aesthetically pleasing web solutions, and I continuously seek to refine my skills to deliver impactful user experiences.

For a long time, unit testing was the most ignored part of my React learning journey.

Not because I didn’t care about quality — but because testing always felt abstract.
Most tutorials showed isolated examples that didn’t resemble real applications.

So I decided to change the approach.

Instead of learning unit testing first and building later, I learned React Testing Library (RTL) while implementing it inside a real project.

This article documents everything:

  • mindset

  • setup

  • writing test cases

  • handling user interactions

  • understanding coverage

  • mistakes I made

  • how testing changed my development style

If you’re a React developer who feels uncomfortable with testing — this is for you.


Why I Finally Took Unit Testing Seriously

Three things pushed me:

  1. Interviews
    Almost every React interview includes questions like:

    • How do you test components?

    • Difference between Jest and RTL?

    • How do you test user interaction?

  2. Fear of Refactoring
    Small UI changes were breaking existing functionality.

  3. Realization

    Manual testing does not scale.

At some point, I understood:

Testing is not for testers.
Testing is for developers who want confidence.


My Learning Strategy (Very Important)

Before touching any tool, I fixed my mindset:

❌ What I avoided

  • Memorizing RTL APIs

  • Writing tests just for coverage

  • Testing implementation details

✅ What I focused on

  • Testing what the user sees

  • Testing how the user interacts

  • Testing real behavior

This is exactly what React Testing Library promotes.


Step 1: Understanding What RTL Actually Is

React Testing Library is not about testing React components.

It is about testing:

How your app behaves from the user’s perspective

That means:

  • No checking state directly

  • No accessing component instances

  • No shallow rendering mindset


Step 2: Setting Up Testing in the Project

I already had a React project.
I added RTL with the following packages:

npm install --save-dev @testing-library/react
npm install --save-dev @testing-library/jest-dom

Setup File

// setupTests.js
import '@testing-library/jest-dom';

This enabled readable assertions like:

  • toBeInTheDocument

  • toBeDisabled

  • toHaveTextContent


Step 3: Writing the First Test (Confidence Booster)

Component: Button.jsx

const Button = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};

export default Button;

Test: Button.test.jsx

import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders button with correct label', () => {
  render(<Button label="Click Me" />);

  const button = screen.getByText('Click Me');
  expect(button).toBeInTheDocument();
});

What I Learned Here

  • render() mounts the component

  • screen represents what the user sees

  • Tests read like English

This was the moment testing started making sense.


Step 4: Testing User Interaction (The Core of RTL)

Real apps are interactive.
So I tested click behaviour.

import { render, screen, fireEvent } from '@testing-library/react';

test('calls onClick when button is clicked', () => {
  const handleClick = jest.fn();

  render(<Button label="Submit" onClick={handleClick} />);

  fireEvent.click(screen.getByText('Submit'));

  expect(handleClick).toHaveBeenCalledTimes(1);
});

Key Takeaway

  • jest.fn() helps track function calls

  • fireEvent simulates real user actions

  • We test effects, not implementation


Step 5: Testing a Real Form (Project-Level Example)

Component: LoginForm.jsx

import { useState } from 'react';

const LoginForm = () => {
  const [email, setEmail] = useState('');

  return (
    <>
      <input
        placeholder="Enter email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button disabled={!email}>Login</button>
    </>
  );
};

export default LoginForm;

Test Case

import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('login button enables when email is entered', () => {
  render(<LoginForm />);

  const input = screen.getByPlaceholderText('Enter email');
  const button = screen.getByText('Login');

  expect(button).toBeDisabled();

  fireEvent.change(input, {
    target: { value: 'test@example.com' },
  });

  expect(button).toBeEnabled();
});

Why This Matters

This test:

  • Covers conditional rendering

  • Covers state update

  • Covers UI behaviour

  • Mimics real user flow


Step 6: Learning Query Priority (Very Important)

RTL recommends this order:

  1. getByRole

  2. getByLabelText

  3. getByPlaceholderText

  4. getByText

  5. getByTestId (last option)

Example

screen.getByRole('button', { name: /login/i });

This:

  • Improves accessibility

  • Makes tests more resilient

  • Encourages better HTML semantics


Step 7: Running Coverage Report

npm test -- --coverage

Coverage report shows:

  • Statements

  • Branches

  • Functions

  • Lines

What Coverage Taught Me

  • Lines → Did the code execute?

  • Branches → Did conditions run?

  • Functions → Were functions actually called?

High coverage does not guarantee quality
But low coverage guarantees risk


Step 8: Improving Coverage the Right Way

Instead of fake tests, I added:

Empty State Test

test('error message is not visible initially', () => {
  render(<Form />);
  expect(screen.queryByText('Error')).not.toBeInTheDocument();
});

Conditional Rendering Test

test('shows loader when loading is true', () => {
  render(<Component loading />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Common Mistakes I Made (Learn From This)

❌ Testing internal state
❌ Overusing data-testid
❌ Writing tests only for coverage
❌ Avoiding tests during development

✅ Test behaviour
✅ Test user interaction
✅ Write tests alongside components


How Unit Testing Changed My Development Style

After RTL:

  • I write smaller components

  • I refactor without fear

  • UI bugs reduce significantly

  • Code reviews become easier

  • Interview confidence increased

Testing became part of development, not an afterthought.


Final Thoughts

I didn’t learn React Testing Library by reading documentation.

I learned it by:

  • Breaking things

  • Writing wrong tests

  • Fixing them

  • Seeing coverage improve

  • Gaining confidence

If you’re avoiding unit testing — don’t aim for perfection.
Aim for clarity and confidence.

Start small.
One test.
One component.

That’s how it begins.

More from this blog

C

CodeCraft by Saurabh

6 posts

CodeCraft by Saurabh shares practical articles on React, Next.js, JavaScript, Redux, Tailwind CSS, and more to guide developers through modern frontend challenges and best practices.