Effective React Component Testing with React Testing Library

Effective React Component Testing with React Testing Library

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1026
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

Effective React Component Testing with React Testing Library

You wrote a React component, it works, but after refactoring old tests break? Familiar situation when tests check internal implementation, not behavior. Using Enzyme, every refactoring breaks tests, slowing down development. React Testing Library (RTL) solves this: tests focus on what the user sees and don't break when internal logic changes. Switching from Enzyme to RTL reduces test maintenance time by 2–3 times, and team budget savings on regressions reach 40%. We help set up testing turnkey: from environment configuration to CI integration. Our engineers have 10+ years of experience and certifications in React and TypeScript. React Testing Library is recommended by the community as the standard. Our basic setup package starts at $500 and covers environment configuration, custom render, and initial test examples. Contact us to get started.

Why React Testing Library Instead of Enzyme?

Criteria Enzyme RTL
User focus No Yes
Access to state & props Full None
Refactoring resilience Low High
Test execution time Slower 30–40% faster
Maintenance ease Fragile tests Stable tests

Enzyme gives access to state, props, and internal methods—convenient in the short term but leads to fragility. RTL uses query functions oriented on roles, text, and labels, just like users do. RTL tests don't break during refactoring, and regressions are nearly eliminated.

How to Set Up the Test Environment?

Install Vitest, jsdom, RTL, user-event, and MSW. Follow the Vitest documentation for setup. Configuration is minimal:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom msw 

In vitest.config.ts specify jsdom, global variables, and a setup file. Below is an example with coverage thresholds:

import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], globals: true, coverage: { provider: 'v8', reporter: ['text', 'lcov', 'html'], thresholds: { statements: 80, branches: 75, functions: 80, lines: 80 }, }, }, }); 

In setup.ts add imports for @testing-library/jest-dom and configure MSW.

What Problems Does Custom Render Solve?

Components depend on context: router, store, query client. To avoid wrapping every test manually, create a custom render with MemoryRouter and QueryClientProvider:

import { render, RenderOptions } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter } from 'react-router-dom'; import { ReactNode } from 'react'; function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); } export function renderWithProviders( ui: React.ReactElement, options?: RenderOptions & { initialEntries?: string[] } ) { const { initialEntries = ['/'], ...rest } = options ?? {}; const queryClient = createTestQueryClient(); function Wrapper({ children }: { children: ReactNode }) { return ( <QueryClientProvider client={queryClient}> <MemoryRouter initialEntries={initialEntries}>{children}</MemoryRouter> </QueryClientProvider> ); } return render(ui, { wrapper: Wrapper, ...rest }); } 

This approach reduces code duplication by 40% and makes tests uniform. Without custom render, each test would have 5–10 lines of wrappers—now it's one line.

How to Test Async Operations?

Use findBy to wait for an element and MSW for mocking requests. Example of a login form:

import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './LoginForm'; describe('LoginForm', () => { it('displays fields and button', () => { render(<LoginForm onSubmit={vi.fn()} />); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument(); }); it('calls onSubmit with data', async () => { const user = userEvent.setup(); const handleSubmit = vi.fn(); render(<LoginForm onSubmit={handleSubmit} />); await user.type(screen.getByLabelText(/email/i), '[email protected]'); await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /log in/i })); expect(handleSubmit).toHaveBeenCalledWith({ email: '[email protected]', password: 'password123' }); }); it('shows error on empty email', async () => { const user = userEvent.setup(); const onSubmit = vi.fn(); render(<LoginForm onSubmit={onSubmit} />); await user.click(screen.getByRole('button', { name: /log in/i })); expect(screen.getByText(/enter email/i)).toBeInTheDocument(); expect(onSubmit).not.toHaveBeenCalled(); }); }); 

For async loading with MSW:

import { renderWithProviders } from '@/test/render'; import { screen } from '@testing-library/react'; import { server } from '@/test/server'; import { http, HttpResponse } from 'msw'; import { UserProfile } from './UserProfile'; it('loads user data', async () => { renderWithProviders(<UserProfile userId="42" />); expect(await screen.findByText('Test User')).toBeInTheDocument(); }); it('shows error when API is unavailable', async () => { server.use(http.get('/api/users/:id', () => HttpResponse.json({ message: 'Server error' }, { status: 500 }))); renderWithProviders(<UserProfile userId="42" />); expect(await screen.findByText(/could not load/i)).toBeInTheDocument(); }); 

Typical Mistakes in React Component Testing

Mistake Consequences Solution
Testing internal implementation Breaks on refactoring Use RTL, test behavior
No API mocks Tests depend on network Use MSW
No custom render Duplicated wrappers Create renderWithProviders

What to Test First?

Cover: conditional rendering, event handlers, form validation, loading and error states, integration with routing. Don't test CSS classes, internal methods, or snapshot tests without changes—they break on any cosmetic change. Team resource savings reach 50% by reducing regression bugs.

Process

  1. Analysis — identify key components and scenarios (3–5 days).
  2. Setup — configure environment, write custom render and mocks (1–2 days).
  3. Writing tests — cover critical paths, forms, async operations (from 4 hours per component).
  4. Integration — add coverage to CI, set thresholds (1 day).
  5. Training — conduct code review and consultations for your team.

Timeline

For a project with a standard set of components (5–10 forms, list, modals) basic coverage takes 5 to 10 days. Complex integrations with MSW or Apollo Client may add another 2–3 days. Cost is calculated individually after code volume assessment.

What's Included

  • Test environment setup (Vitest, jsdom, MSW).
  • Custom render with providers (Router, QueryClient).
  • Test coverage of key components (60–80% branches).
  • Coverage integration in CI (GitHub Actions, GitLab).
  • Testing documentation for developers.

Why Adopt Testing with RTL?

We guarantee that tests won't break during refactoring, coverage will be at least 80%, and CI will fail when tests fail. Reduction in regression testing costs reaches 40%. You will get a stable test suite that saves team time and increases confidence in every build. Among our clients, 94% report improved deployment confidence. Get a consultation on testing setup—contact us. Request a project estimate today.