Skip to content

Unit Testing React Applications

At the end of this class, you should be able to…

  • Set up and configure Vitest and React Testing Library for React projects.
  • Write unit tests for components to validate behaviour and rendered output.
  • Interpret test results and debug failed test cases effectively.

Additional Notes:

  • Emphasize that unit tests verify behaviour rather than implementation details.

This lesson introduces unit testing using Vitest and React Testing Library.

We will:

  • understand why testing is important
  • set up a testing environment
  • write our first test
  • test component rendering and behaviour
  • apply testing to existing components

So far, we have focused on building features.

Now we ask:

How do we verify that our application works correctly?

Testing helps us:

  • prevent bugs
  • verify behaviour
  • refactor safely

Key idea:

Tests give us confidence when changing code

We focus on:

  • what the user sees
  • what the user does

We avoid:

  • testing implementation details
  • testing internal state directly

Key idea:

Test behaviour, not implementation

Add a test script to your package.json:

"scripts": {
"test": "vitest"
}

Create a config file vitest.config.js if needed:

import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});

Create a new test file:

src/components/Header.test.jsx

Add:

import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import Header from './Header';
test('renders the application title', () => {
render(<Header />);
expect(screen.getByText(/NAIT Resource Directory/i)).toBeTruthy();
});

Run tests:

Terminal window
npm run test

You will receive an error that React is not defined.

Remember, React is not a native browser object, we need to ensure that the environment is configured to make use of it.

To enable React support in our tests, install the vite React plugin:

Terminal window
npm i -D @vitejs/plugin-react

Then, update your vitest config file:

import react from '@vitejs/plugin-react'; // import the package
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
plugins: [react()], // configure the plugin
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});

Run the test again:

Terminal window
npm test

Close, but no we are running into another issue, reliance on higher dependencies. In this case, because the Header requires a ThemeContext, the test fails becasue we are rendering the Header without first wrapping it in the necessary Provider. To fix this, and keep our tests localized to this unit only, we need to include a ThemeProvider for the test.

The fix here is to create our own custom render function, one that will ensure any external requirements the component we’re testing are present.

In the src/ directory, create a vitest-helpers.jsx file:

import { ThemeContext } from './context/ThemeContext';
import { render } from '@testing-library/react';
// mock theme context value
const mockThemeContextValue = {
theme: 'light',
toggleTheme: () => { },
};
export function renderWithProviders(ui, options) {
return render(
<ThemeContext.Provider value={mockThemeContextValue}>
{ui}
</ThemeContext.Provider>, { ...options }
);
}

Now, we have a place to add any additional environment setup required to test our components.

Back in Header.test.jsx, let’s put the new render function to work:

import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../vitest-helpers'; // import the new render function
import Header from './Header';
test('renders the application title', () => {
renderWithProviders(<Header />); // using our new render function
expect(screen.getByText(/NAIT Resource Directory/i)).toBeTruthy();
});

At this point, we are still going to run into one issue. Any idea why? Take a look in the Header.jsx file. What else do you see that could cause problems? If you run the test script, you will get a hint: Error: useLocation() may be used only in the context of a <Router> component.. Yup, the React Router dependency also needs to be accounted for. In this case, we can create a temporary MemoryRouter to quickly satisfy the testing environent requirements. Update Header.test.jsx once again:

import { MemoryRouter } from 'react-router'; // import the package
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../vitest-helpers';
import Header from './Header';
test('renders the application title', () => {
// wrap the Header in the necessary router
renderWithProviders(
<MemoryRouter>
<Header />
</MemoryRouter>);
expect(screen.getByText(/NAIT Resource Directory/i)).toBeTruthy();
});

The test should now run successfully.

Note: for testing components that require route params, you can modify the MemoryRouter’s initialEntries prop and use a <Route> to work params into your tests.

You can make use of the screen object to query for elements and, where supported, use HTMLElement functions to trigger events (e.g., click()), or you can fire events using TestingLibrary:

import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
test('calls handler when button is clicked', async () => {
let clicked = false;
render(<button onClick={() => (clicked = true)}>Click</button>);
await screen.getByText('Click').click(); // HTMLElement.click()
expect(clicked).toBe(true);
});

Any component that requires props can easily be tested as well. Create a src/components/Details.test.jsx test file for the Details component:

import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import Details from './Details';
test('displays resource details', () => {
const resource = {
title: 'Math Help Centre',
category: 'Academic Support'
};
render(<Details resource={resource} />);
expect(screen.getByText('Math Help Centre')).toBeTruthy();
});

Note: Since the Details component doesn’t rely on any providers, we can use the default render function here.

Test conditional rendering by putting your component into an expected state and then run your assertions:

import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import Details from './Details';
...
test('shows placeholder when no resource is selected', () => {
render(<Details resource={null} />); // no resource should render a message
expect(screen.getByText(/select a resource/i)).toBeTruthy();
});
  • Tests should be simple and focused
  • Each test verifies one behaviour
  • Use queries that match how users interact
  1. Create a new test file for the Results component.
    • Verify that a list of resources renders correctly when provided as props.
  2. Write a test that verifies clicking a resource in the Results list triggers the expected selection behaviour.
    • You may need to pass a mock handler function and assert that it was called.
  3. Add a test for a component that includes conditional UI (e.g., empty state, loading state, or fallback message).
    • Verify that the correct message is displayed when no data is available.
  4. Create a test for a form component (e.g., ResourceForm).
    • Verify that input values can be entered.
    • Verify that submitting the form triggers the expected handler.
  5. Refactor one of your tests to improve readability.
    • Use clearer test names.
    • Ensure each test focuses on a single behaviour.
Terminal window
git add -A
git commit -m "Lesson 27 - unit testing walkthrough"
git push origin main