Unit Testing Guidelines

Unit tests are the foundation of our testing strategy. They are small, fast, and test a single “unit” of code in isolation. Our primary tools for unit testing are Jest and React Testing Library.

What to Test

  • React Components:
    • Test that the component renders correctly given a set of props.
    • Test that user interactions (like clicks or form inputs) trigger the correct events or state changes.
    • Test conditional rendering logic (e.g., does the “Loading…” message appear when isLoading is true?).
  • Utility Functions:
    • Test that the function returns the correct output for a given input.
    • Test edge cases (e.g., what happens if the input is null, undefined, or an empty array?).
  • API Routes / Server-side Logic:
    • Test the logic of your server-side functions, mocking any external dependencies like databases or other APIs.

What NOT to Test

  • Implementation Details: Don’t test the internal state or methods of a component. Test the component from the user’s perspective (i.e., what they see and can interact with).
  • Third-Party Libraries: Don’t test that a third-party library (like a date picker) works. Assume it does. Test that your code is interacting with it correctly.
  • Trivial Code: Don’t write tests for code that has no logic (e.g., a simple component that just renders a title).

Best Practices

The AAA Pattern: Arrange, Act, Assert

Structure your tests in three distinct parts:

  1. Arrange: Set up the test. Render the component with the necessary props, or create the inputs for your function.
  2. Act: Perform the action you want to test (e.g., click a button, call the function).
  3. Assert: Check that the outcome is what you expected.

Example (React Component):

import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
 
test('increments the count when the button is clicked', () => {
  // 1. Arrange
  render(<Counter />);
  const button = screen.getByRole('button', { name: /increment/i });
  const countDisplay = screen.getByText(/count is/i);
 
  // 2. Act
  fireEvent.click(button);
 
  // 3. Assert
  expect(countDisplay).toHaveTextContent('Count is 1');
});

Writing Good Assertions

  • Be specific: expect(user.name).toBe('Alice') is better than expect(user).toBeDefined().
  • Use semantic matchers: React Testing Library provides excellent, user-centric matchers.
    • Prefer getByRole, getByLabelText, getByText.
    • Avoid getByTestId unless there’s no other way to get the element.

Mocking

  • Jest’s Mocking Functions: Use jest.fn() to create mock functions and jest.spyOn() to spy on or mock existing functions.
  • Mocking Modules: Use jest.mock('./path/to/module') to mock entire modules. This is essential for isolating your unit under test from its dependencies (like API clients or other services).
  • File-based Mocking: For mocking API responses, you can place mock files in a __mocks__ directory adjacent to the module you’re mocking.

File Location and Naming

  • Test files should be located alongside the files they are testing.
  • The file name should be [filename].test.ts or [filename].test.tsx.

Example:

/components
  /Button
    - Button.tsx
    - Button.test.tsx

By writing good unit tests, we can build a safety net that allows us to refactor code and add new features with confidence.