ReactJs
React Testing

React Testing

1. Unit Testing with Jest

Concept: Unit testing involves testing individual units of code—like functions or components—in isolation. Jest is a JavaScript testing framework that makes it easy to write and run unit tests for React components and JavaScript functions.

Example:

Suppose you have a React component that performs a complex calculation and displays results based on user input. You want to test the component to ensure it calculates and displays results correctly.

import React, { useState } from 'react';
 
const ComplexCalculation = ({ multiplier }) => {
  const [input, setInput] = useState(0);
 
  const handleInputChange = (e) => {
    const value = parseInt(e.target.value, 10);
    setInput(isNaN(value) ? 0 : value);  // Handle NaN gracefully
  };
 
  const result = input * multiplier;
 
  return (
    <div>
      <input
        type="number"
        value={input}
        onChange={handleInputChange}
        aria-label="calculation-input"  // Added an accessible label
      />
      <p aria-label="calculation-result">Result: {result}</p>  {/* Added aria label for accessibility */}
    </div>
  );
};
 
export default ComplexCalculation;
 

Unit Test with Jest:

Here’s how you might write a unit test for this component using Jest:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // for the matchers like "toBeInTheDocument"
import ComplexCalculation from './ComplexCalculation';
 
 
describe('ComplexCalculation Component', () => {
  
  test('renders with default multiplier and input of 0', () => {
    render(<ComplexCalculation multiplier={5} />);
    
    const resultElement = screen.getByText(/Result: 0/i);
    expect(resultElement).toBeInTheDocument();
  });
 
  test('calculates and displays the correct result based on multiplier and input', () => {
    render(<ComplexCalculation multiplier={5} />);
    
    const inputElement = screen.getByLabelText('calculation-input');
    fireEvent.change(inputElement, { target: { value: '3' } });
    
    const resultElement = screen.getByText(/Result: 15/i);
    expect(resultElement).toBeInTheDocument();
  });
 
  test('handles different multiplier values correctly', () => {
    render(<ComplexCalculation multiplier={10} />);
    
    const inputElement = screen.getByLabelText('calculation-input');
    fireEvent.change(inputElement, { target: { value: '2' } });
    
    const resultElement = screen.getByText(/Result: 20/i);
    expect(resultElement).toBeInTheDocument();
  });
 
  test('handles non-numeric input gracefully by defaulting to 0', () => {
    render(<ComplexCalculation multiplier={5} />);
    
    const inputElement = screen.getByLabelText('calculation-input');
    fireEvent.change(inputElement, { target: { value: 'abc' } });
    
    const resultElement = screen.getByText(/Result: 0/i);
    expect(resultElement).toBeInTheDocument();
  });
 
  test('handles negative numbers correctly', () => {
    render(<ComplexCalculation multiplier={4} />);
    
    const inputElement = screen.getByLabelText('calculation-input');
    fireEvent.change(inputElement, { target: { value: '-3' } });
    
    const resultElement = screen.getByText(/Result: -12/i); 
    expect(resultElement).toBeInTheDocument();
  });
 
  test('renders snapshot correctly', () => {
    const { asFragment } = render(<ComplexCalculation multiplier={2} />);
    
    expect(asFragment()).toMatchSnapshot();
  });
});
 

Explanation:

  • Initial State Test: Verifies that the initial result is correct when the input is zero.
  • Input Change Test: Checks if the result updates correctly when the input value changes.
  • Different Prop Test: Tests how the component behaves with different multiplier values.

2. Integration Testing with React Testing Library

Concept: Integration testing ensures that various parts of your application work together as expected. React Testing Library provides utilities to render components and interact with them, simulating user interactions and verifying results.

Complex Example:

Consider a component that relies on another component to fetch data and display it:

// UserProfile.js
import React, { useEffect, useState } from 'react';
import UserDetail from './UserDetail';
 
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    // Mock API call
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    };
    
    fetchUser();
  }, [userId]);
 
  return user ? <UserDetail user={user} /> : <p>Loading...</p>;
};
 
export default UserProfile;

Integration Test with React Testing Library:

Here’s an integration test that checks if UserProfile correctly fetches and displays user details:

// UserProfile.test.js
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
 
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ name: 'John Doe', age: 30 }),
  })
);
 
describe('UserProfile Component', () => {
  test('should display user details once data is fetched', async () => {
    render(<UserProfile userId={1} />);
    
    // Verify loading state
    expect(screen.getByText('Loading...')).toBeInTheDocument();
    
    // Wait for the user details to be rendered
    await waitFor(() => expect(screen.getByText('John Doe')).toBeInTheDocument());
    expect(screen.getByText('Age: 30')).toBeInTheDocument();
  });
});

Explanation:

  • Mocking API Calls: Uses Jest to mock the fetch function and simulate API responses.
  • Loading State Verification: Checks if the loading state is displayed initially.
  • Data Rendering Check: Waits for the component to display user details once data is fetched.

3. End-to-End Testing with Cypress

Concept: End-to-end (E2E) testing simulates real user interactions and verifies that the entire application functions correctly from start to finish. Cypress is a popular E2E testing tool that runs tests in a real browser.

Complex Example:

Consider a scenario where you need to test the complete login and dashboard flow of a web application:

// e2e.spec.js
describe('User Login and Dashboard Flow', () => {
  before(() => {
    // Setup any necessary test data or environment
  });
 
  it('should allow a user to log in and navigate to the dashboard', () => {
    cy.visit('/login');
    
    // Fill login form
    cy.get('input[name="username"]').type('testuser');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    
    // Verify successful login and redirection
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome, testuser!');
    
    // Interact with dashboard
    cy.get('button').contains('View Profile').click();
    cy.url().should('include', '/profile');
    cy.contains('Profile Details');
  });
});

Explanation:

  • Visit and Interaction: Navigates to the login page, fills out the form, and submits it.
  • URL and Content Verification: Ensures the user is redirected to the dashboard and the correct welcome message is displayed.
  • Further Interaction: Simulates clicking on a button to view the profile and verifies that the profile page is correctly displayed.

Summary:

  • Unit Testing with Jest: Tests individual components or functions for correctness in isolation, with a focus on functionality and state.
  • Integration Testing with React Testing Library: Ensures components work together as expected and integrates with the DOM, simulating user interactions and validating component behavior.
  • End-to-End Testing with Cypress: Validates complete user flows and application functionality, ensuring that all parts of the application work together correctly and the user experience is as intended.