Welcome to the EpicWeb.dev Workshop app!

This is the deployed version. Run locally for full experience.

Component Testing

When building React components we want to focus on our two users:
  1. The end user who's typing in the fields and clicking the buttons
  2. The developer user who's rendering our component with props
We want to avoid a "third" user I call the "Test User" (which we've mentioned earlier). So we want to avoid mocking out React and other hooks if we can help it. We'll do that by using the render function from @testing-library/react.
@testing-library/react is the de-facto standard library for testing React components. It's based on @testing-library/dom which is the basis of testing library implementations for other frameworks as well. I'm actually the author of Testing Library! I wrote it when preparing a workshop like this. It's a set of utilities that allow you to embrace our testing philosophy:
The more your tests resemble the way your software is used, the more confidence they can give you. - @kentcdodds
So, if this is your first time using @testing-library/react, I recommend having the documentation open and ready to reference as you go through this and the next exercise (especially the page about queries). Much of it will feel familiar to what we did in the Playwright exercises because Playwright's queries were inspired by Testing Library's queries ๐ŸŽ‰.
NOTE: In the near future, React Testing Library will have a completely async API (#1214). The videos were recorded with this implemented and the version of Testing Library you have in this workshop app has this implemented as well. However, if you use the latest version of Testing Library, you will not have this implemented yet. Things should work either way, but that's why we're adding await to our render and act calls.
Here's the simplest example of a test using Testing Library:
import * as React from 'react'

export function Counter() {
	const [count, setCount] = React.useState(0)
	const increment = () => setCount(c => c + 1)
	return <button onClick={increment}>Count: {count}</button>
import { expect, test } from 'vitest'
import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { Counter } from './counter.tsx'

test('counter increments when clicked', async () => {
	await render(<Counter />)
	const button = screen.getByRole('button', { name: /count:/i })
	expect(button.textContent).toBe('Count: 0')
	await userEvent.click(button)
	expect(button.textContent).toBe('Count: 1')
If you've not used @testing-library/react before, I recommend you check out the Quick start Example in the docs which should give you a pretty good idea of how to use it effectively.
If you've used Testing Library in the past, you may be interested in reading Common mistakes with React Testing Library as you may be making them yourself (don't feel bad, they're common ๐Ÿ˜…).

Simulated DOM

Vitest runs in Node.js, but our components run in a browser. You can actually run vitest in the browser, but it's still experimental so we're not going to learn that today, but the documentation page about it is a good read if you want to understand the challenges with running in a simulated environment like we do.
What you should know is that in order to simulate a browser environment, we use a library called jsdom. This library has many known limitations and differences from real browsers. However, it has the benefit of being lighter weight than a full browser and therefore it starts up much faster. So it's well suited for lower level tests like the one's we'll be using it for today.
That said, we don't want it to be set up by default for all our vitest tests, so we enable it on a per-test basis using the @vitest-environment comment pragma at the top of our UI test files:
 * @vitest-environment jsdom
Adding this to a test file tells vitest to initialize jsdom in the global environment before running our test file (so we can safely use document, etc. in our tests).

DOM Assertions

The expect library that we're using with vitest is extensible (we'll learn more about this later). There are existing extensions for testing library called @testing-library/jest-dom and they're really nice to use. So we'll use them in our tests.
import { expect, test } from 'vitest'
import '@testing-library/jest-dom'

// ...

test('counter increments when clicked', async () => {
	// ...
	expect(button).toHaveTextContent('Count: 0')
	await userEvent.click(button)
	expect(button).toHaveTextContent('Count: 1')