React Testing Library + Vitest Setup and Component Testing Principles

What is the React Testing Library?

The React Testing Library has special tools to test how your website looks and behaves from a user’s perspective. It checks if buttons work when clicked and if input boxes accept text correctly.

Instead of testing everything by hand, which can be slow and might miss things as your website scales up, you can write tests and run them all at once with just one command.

Component Testing Principles

What to TestWhat Not to Test
Make sure elements are present/absent in the document,
Confirm expected behaviors for user actions like clicking a button or updating text in an input box and so on
Internal state of a component,
Internal methods of a component,
Lifecycle methods of a component,
Child components

Recommended Best Practices

  • Act like a user would. Instead of using fireEvent(), use “@testing-library/user-event”. it offers an experience closer to that of a real user’s interactions with clicking and other actions
  • Don’t make a separate example app for testing components. Just import the component directly and test it.
  • Keep your test files alongside your components. For example, if you have a file named example.jsx, create a test file named example.test.jsx in the same directory. This makes it easier to track test coverage over time.
  • Follow test-driven development (TDD): Write your tests first, then create the component.

Install Dependencies

To start, install the dependencies related to React Testing Library:

After running the above command, these devDependencies should be automatically added to the package.json file.

// package.json

"devDependencies": {
      "@testing-library/jest-dom": "^5.17.0",
      "@testing-library/react": "^13.4.0",
      "@testing-library/user-event": "^13.5.0",
      "@types/jest": "^27.5.2",
      "@types/testing-library__jest-dom": "^5.14.8",
      "@vitejs/plugin-react": "^4.2.1",
      "jsdom": "^24.0.0",
      "vite": "^5.1.3",
      "vitest": "^1.3.1"
  }

Configuration

Create a file named vite.config.js in the root directory. The code below shows the very minimum configuration needed for setting up React Testing Library + Vitest.

// vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    test: {
        globals: true,
        environment: 'jsdom',
    },
})
JavaScript

Code Example

Instead of long explanations, skilled developers grasp concepts better through real code examples.

Basic Example:

import { expect, describe, test, vi } from 'vitest'
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'
import App from './App'

describe('Heading component', () => {
    test('should render App component correctly', () => {
        render(<App />)

        act(() => {
            const btn = screen.getByRole('button', {
                name: /increase/i,
            })
            userEvent.click(btn)
        })

        const outputDiv = screen.getByText(/Zoo has 10 bears\./i)
        expect(outputDiv).to.exist
    })
})
JavaScript

import { render, screen } from '@testing-library/react'
import { describe, test } from 'vitest'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'
import Register from './Register'

describe('Register component', () => {
    it('should render Register component correctly', 
        () => {
        render(<Register />)
        const element = screen.getByRole('heading', {
            level: 2,
        })
        expect(element).toBeInTheDocument()
    })

    it('should test for presence of subheading in the component', 
        () => {
        render(<Register />)
        const element = screen.getByRole('heading', {
            name: /please enter your details below to register yourself\./i,
        })
        expect(element).toBeInTheDocument()
    })

    it('should show error message when all the fields are not entered', 
        async () => {
        render(<Register />)
        const buttonElement = screen.getByRole('button', {
            name: /register/i,
        })
        await userEvent.click(buttonElement)
        const alertElement = screen.getByRole('alert')
        expect(alertElement).toBeInTheDocument()
    })

    it('should not show any error message when the component is loaded', 
        () => {
        render(<Register />)
        const alertElement = screen.queryByRole('alert')
        expect(alertElement).not.toBeInTheDocument()
    })

    it('should show success message when the registration is successful.', 
        async () => {
        render(<Register />)
        const buttonElement = screen.getByRole('button', {
            name: /register/i,
        })
        await userEvent.click(buttonElement)
        const alertElement = screen.getByRole('alert')
        expect(alertElement).toBeInTheDocument()
    })
})
JavaScript

Example with Mocks (.tsx)

import Heading from './Heading'
import { expect, describe, test, vi } from 'vitest'
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'

// Test suite for the Heading component
describe('Heading component', () => {
    // Spies
    let onCloseSpy: jest.SpyInstance<void, []> | null
    let onEditSpy: jest.SpyInstance<void, [string]> | null

    // Set up before each test
    beforeEach(async () => {
        // Mock necessary modules and functions
        vi.mock('../init', () => {
            return {
                actions: {
                    onClose: () => {},
                    onEdit: (src: string) => 'FAKE_IMAGE_URL',
                },
            }
        })

        vi.mock('../store', () => {
            return {
                setStatus: (status: Boolean) => {},
            }
        })

        vi.mock('./crop', () => {
            return {
                getData: () => {},
            }
        })

        // Import the module and set up spies
        const actionsModule = await import('./init')
        onCloseSpy = vi.spyOn(actionsModule.actions, 'onClose') 
        onEditSpy = vi.spyOn(actionsModule.actions, 'onEdit') 
    })

    // Clean up after each test
    afterEach(() => {
        vi.restoreAllMocks()
    })

    // Test case: should render Heading component correctly
    test('should render Heading component correctly', () => {
        // Render the Heading component
        render(<Heading />)

        // Assert that the necessary elements are rendered
        const heading = screen.getByRole('heading', {
            name: 'Image Section',
        })
        expect(heading).toBeInTheDocument()

        const cancelButton = screen.getByRole('button', {
            name: 'Cancel',
        })
        expect(cancelButton).toBeInTheDocument()

        const saveButton = screen.getByRole('button', {
            name: 'Save',
        })
        expect(saveButton).toBeInTheDocument()
    })

    // Test case: should handle click actions correctly
    test('should handle click actions correctly', async () => {
        // Render the Heading component
        render(<Heading />)

        // Simulate clicking the "Cancel" button
        await act(async () => {
            const cancelButton = screen.getByRole('button', {
                name: 'Cancel',
            })
            userEvent.click(cancelButton)
        })

        // Verify that onClose function is called
        expect(onCloseSpy).toHaveBeenCalledTimes(1)

        // Simulate clicking the "Save" button
        await act(async () => {
            const saveButton = screen.getByRole('button', {
                name: 'Save',
            })
            userEvent.click(saveButton)
        })

        // Verify that onEdit function is called with expected parameter
        expect(onEditSpy).toHaveBeenCalledTimes(1)
        expect(onEditSpy).toHaveBeenCalledWith('FAKE_IMAGE_URL')
    })
})
JavaScript

Example with Advanced Events (.tsx)

import ToolButton from './ToolButton'
import { expect, describe, test, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import userEvent from '@testing-library/user-event'

// Test suite for the ToolButton component
describe('ToolButton component', () => {
    // Test case: should render ToolButton component correctly
    test('should render ToolButton component correctly', async () => {
        // Render the ToolButton component with custom text
        render(
            <ToolButton>
                <span>Test Button</span>
            </ToolButton>
        )

        // Assert that the necessary elements are rendered
        const toolButton = screen.getByRole('button', { name: 'Test Button' })
        expect(toolButton).toBeInTheDocument()
    })

    // Test case: should execute press and click actions on ToolButton component correctly
    test('should execute press and click actions correctly', 
        async () => {
        let fakePressEventCallCount = 0
        const fakeClickEvent = vi.fn()
        const fakePressEvent = vi.fn(() => {
            fakePressEventCallCount++
        })

        // Render the ToolButton component with custom text and custom events
        render(
            <ToolButton onClick={fakeClickEvent} onPress={fakePressEvent}>
                <span>Test Button</span>
            </ToolButton>
        )

        // Simulate clicking the "Test" button
        await act(async () => {
            const toolButton = screen.getByRole('button', { name: 'Test Button' })
            userEvent.click(toolButton)
        })

        // Assert that the click event is called once
        expect(fakeClickEvent).toBeCalledTimes(1)

        // Simulate pressing and holding the "Test" button for 0.325 seconds
        await act(async () => {
            const toolButton = screen.getByRole('button', { name: 'Test Button' })
            
            // Wait for 1 second to simulate holding the mouse press
            fireEvent.mouseDown(toolButton)
            await new Promise((resolve) => setTimeout(resolve, 1000)) // wait for 1 second
            fireEvent.mouseUp(toolButton)
        })

        // The "Test" button should be pressed around 60 times within 1000ms.
        // While accuracy is not critical for this test, it should be reasonably reliable.
        // Therefore, setting the expectation to be greater than 30 to ensure test pass reliably.
        expect(fakePressEventCallCount).toBeGreaterThan(30)
    })
})
JavaScript

Running Tests

In your package.json file, add these lines under the scripts section:

For watch mode: "test": "vitest"

For one-time execution mode: "test": "vitest --run""

References

React Testing Library Tutorial – How to Write Unit Tests for React Apps

Leave a Reply

Your email address will not be published. Required fields are marked *