Unit Testing React Applications
Learning Outcome Guide
Section titled “Learning Outcome Guide”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.
Lesson focus
Section titled “Lesson focus”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
Connecting to prior lessons
Section titled “Connecting to prior lessons”So far, we have focused on building features.
Now we ask:
How do we verify that our application works correctly?
Phase 1: Why test?
Section titled “Phase 1: Why test?”Testing helps us:
- prevent bugs
- verify behaviour
- refactor safely
Key idea:
Tests give us confidence when changing code
Phase 2: Testing philosophy
Section titled “Phase 2: Testing philosophy”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
Phase 3: Configure Vitest
Section titled “Phase 3: Configure Vitest”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' }], }, },});Phase 4: First test
Section titled “Phase 4: First test”Create a new test file:
src/components/Header.test.jsxAdd:
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:
npm run testYou 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:
npm i -D @vitejs/plugin-reactThen, update your vitest config file:
import react from '@vitejs/plugin-react'; // import the packageimport { 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:
npm testClose, 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 valueconst 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 functionimport 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 packageimport { 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
initialEntriesprop and use a<Route>to work params into your tests.
Phase 5: Testing behaviour
Section titled “Phase 5: Testing behaviour”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);});Phase 6: Testing props
Section titled “Phase 6: Testing props”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.
Phase 7: Conditional rendering
Section titled “Phase 7: Conditional rendering”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();});Phase 8: Key concepts
Section titled “Phase 8: Key concepts”- Tests should be simple and focused
- Each test verifies one behaviour
- Use queries that match how users interact
Student Exercise
Section titled “Student Exercise”- Create a new test file for the Results component.
- Verify that a list of resources renders correctly when provided as props.
- 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.
- 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.
- 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.
- Refactor one of your tests to improve readability.
- Use clearer test names.
- Ensure each test focuses on a single behaviour.
Push to your GitHub workbook repo
Section titled “Push to your GitHub workbook repo”git add -Agit commit -m "Lesson 27 - unit testing walkthrough"git push origin main