1. File and Folder Naming Conventions for Tests

  • Test file naming conventions:
  • Use extensions like .test.js, .spec.js, or .spec.jsx for test files.
  • Keep all test files inside a dedicated folder named __tests__. Files in this folder are automatically recognized as test files by most test runners (like Jest).
  • Example folder structure:
src/
├── components/
│   ├── App.js
│   ├── __tests__/
│   │   └── App.test.js

2. Testing Input and Button Events

Testing onChange Event for Input Fields

test('testing input', () => {
  render(<App />);
  const input = screen.getByRole('textbox');
  fireEvent.change(input, { target: { value: 'a' } });
  expect(input.value).toBe('a');
});
  • Explanation:
  • fireEvent.change is used to simulate a change event on the input field.
  • The assertion expect(input.value).toBe('a') checks if the value of the input field is updated correctly.

Testing onClick Event for Buttons

test('click event test case', () => {
  render(<App />);
  const btn = screen.getByRole('button');
  fireEvent.click(btn);
  expect(screen.getByText('updated data')).toBeInTheDocument();
});
  • Explanation:
  • fireEvent.click simulates a click on the button.
  • The assertion checks if the text "updated data" appears on the screen after the button click.

3. Lifecycle Hooks: beforeEach, afterEach, beforeAll, afterAll

  • Why we need these hooks:
  • Lifecycle hooks help in setting up and tearing down a consistent environment before and after each test case runs.
  • Hooks usage example:
beforeEach(() => {
  // Runs before each test case
  console.log('Setup for each test');
});

afterEach(() => {
  // Runs after each test case
  console.log('Cleanup after each test');
});

beforeAll(() => {
  // Runs once before all test cases
  console.log('Setup before all tests');
});

afterAll(() => {
  // Runs once after all test cases
  console.log('Cleanup after all tests');
});

4. Snapshot Testing

  • What is snapshot testing?
  • Snapshot testing ensures that the UI of a component does not change unexpectedly. It captures a "snapshot" of the rendered component and compares it with future snapshots.
  • When to use snapshot testing?
  • Use it for pure presentational components or components with minimal logic.
  • Avoid using it for components with dynamic content or rapidly changing UI, as snapshots may become brittle.
  • Example of snapshot testing:
test('snapshot test for App component', () => {
  const container = render(<App />);
  expect(container).toMatchSnapshot();
});
  • Updating snapshots:
  • If there are intended UI changes, run the following command to update the snapshots:
jest --updateSnapshot
  • Benefits of snapshot testing:
  • Helps in detecting unintended UI changes during development.
  • Useful in maintaining consistent UI during production.

5. What to Test and What to Avoid

What to test:

  1. Components:
  • Ensure proper rendering of UI elements.
  • Test props, states, and events.
  1. Functions:
  • Test utility functions and pure functions separately.
  1. UI interactions:
  • Test user interactions such as clicks, form submissions, and API calls.
  1. API calls:
  • Use mocking (e.g., jest.fn() or msw) to simulate API responses and test how components handle them.

What to avoid:

  1. External libraries:
  • Avoid testing third-party libraries, as they already come with their own tests.
  1. Default React functionality:
  • No need to test React’s built-in hooks like useState or useEffect.

6. Class Component Testing

  • Why test class components?
  • Even though functional components are more common, class components are still used in legacy codebases.
  • Testing class components involves checking their lifecycle methods (componentDidMount, componentDidUpdate, etc.) and internal state changes.
  • Example of testing a class component method:
class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

test('increments count on button click', () => {
  const { getByText } = render(<Counter />);
  const button = getByText('Increment');
  fireEvent.click(button);
  expect(getByText('Count: 1')).toBeInTheDocument();
});

7. Functional Component Method Testing

  • Why test functional components?
  • Functional components are now the standard in React development.
  • Methods inside functional components cannot be directly tested, so event-driven testing is used.
  • How to implement testing for functional component methods:
  • Since functional components rely on hooks, you can simulate events that trigger state changes and assert the output.
  • Example:
function App() {
  const [data, setData] = React.useState('');
  return (
    <div>
      <button data-testid="btn1" onClick={() => setData('hello')}>
        Update Data
      </button>
      <h1>{data}</h1>
    </div>
  );
}

test('functional component method testing', () => {
  render(<App />);
  const btn = screen.getByTestId('btn1');
  fireEvent.click(btn);
  expect(screen.getByText('hello')).toBeInTheDocument();
});
  • Alternative approach:
  • Extract reusable functions outside the component, test them separately, and import them into your component.

8. Important Points for Testing

  • Focus on testing core functionality:
  • Core UI rendering and interactions.
  • Component state and props.
  • API integration (mocked).
  • Use mocks and spies:
  • Mock API calls using libraries like jest.fn() or axios-mock-adapter.
  • Use jest.spyOn to spy on functions and ensure they are called correctly.
  • Use act for asynchronous updates:
  • When testing async code or updates caused by promises, wrap them in act to ensure proper handling of state updates.