Component Testing
Loading "Intro to Component Testing"
Run locally for transcripts
When building React components we want to focus on our two users:
- The end user who's typing in the fields and clicking the buttons
- 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')
})