preact-testing-library
Simple and complete Preact DOM testing utilities that encourage good testing practices.
The problem
You want to write maintainable tests for your Preact components. As a part of
this goal, you want your tests to avoid including implementation details of
your components and rather focus on making your tests give you the confidence
for which they are intended. As part of this, you want your testbase to be
maintainable in the long run so refactors of your components (changes to
implementation but not functionality) don't break your tests and slow you and
your team down.
This solution
The preact-testing-library
is a very light-weight solution for testing Preact
components. It provides light utility functions on top of preact
in a way that encourages better testing practices.
It's primary guiding principle is:
[The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
So rather than dealing with instances of rendered Preact components, your tests
will work with actual DOM nodes. The utilities this library provides facilitate
querying the DOM in the same way the user would. Finding for elements by their
label text (just like a user would), finding links and buttons from their text
(like a user would). It also exposes a recommended way to find elements by a
data-testid
as an "escape hatch" for elements where the text content and label
do not make sense or is not practical.
This library encourages your applications to be more accessible and allows you
to get your tests closer to using your components the way a user will, which
allows your tests to give you more confidence that your application will work
when a real user uses it.
This library is a replacement for enzyme. While you
can follow these guidelines using enzyme itself, enforcing this is harder
because of all the extra utilities that enzyme provides (utilities which
facilitate testing implementation details). Read more about this in
the FAQ below.
What this library is not:
- A test runner or framework
- Specific to a testing framework (though we recommend Jest as our
preference, the library works with any framework)
NOTE: This library is built on top of
dom-testing-library
which is where most of the logic behind the queries is. Also this is inspired from
react-testing-library
Table of Contents
Installation
This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's devDependencies
:
npm install --save-dev preact-testing-library
You may also be interested in installing dom-testing-library
so you can use
the custom jest matchers
Usage
import preact from 'preact'
import {render, flushPromises, FireEvent} from '../'
import axiosMock from 'axios'
import Fetch from '../fetch'
test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({
data: {greeting: 'hello there'},
}),
)
const url = '/greeting'
const {getByText} = render(<Fetch url={url} />)
FireEvent.fireEvent(getByText('Fetch'), 'click')
await flushPromises()
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(getByText('hello there').textContent).toBe('hello there')
})
render
Render into a container which is appended to document.body. It should be used with cleanup:
import {render, cleanup} from 'preact-testing-library'
afterEach(cleanup)
render(<div />)
In the example above, the render
method returns an object that has a few
properties:
container
The containing DOM node of your rendered Preact Element (rendered using
Preact.render
). It's a div
. This is a regular DOM node, so you can call
container.querySelector
etc. to inspect the children.
Tip: To get the root element of your rendered element, use container.firstChild
.
debug
This method is a shortcut for console.log(prettyDOM(container))
.
import {render} from 'preact-testing-library'
const HelloWorld = () => <h1>Hello World</h1>
const {debug} = render(<HelloWorld />)
debug()
This is a simple wrapper around prettyDOM which is also exposed and comes from dom-testing-library.
rerender
It'd probably be better if you test the component that's doing the prop updating to ensure that the props are being updated correctly (see the Guiding Principles section). That said, if you'd prefer to update the props of a rendered component in your test, this function can be used to update props of the rendered component.
import {render} from 'preact-testing-library'
const {rerender} = render(<NumberDisplay number={1} />)
rerender(<NumberDisplay number={2} />)
unmount
This will cause the rendered component to be unmounted. This is useful for
testing what happens when your component is removed from the page (like testing
that you don't leave event handlers hanging around causing memory leaks).
This method is a pretty small abstraction over
render(ui, document.body, container)
const {container, unmount} = render(<Login />)
unmount()
getByLabelText(text: TextMatch, options: {selector: string = '*'}): HTMLElement
This will search for the label that matches the given TextMatch
,
then find the element associated with that label.
const inputNode = getByLabelText('Username')
const inputNode = getByLabelText('username', {selector: 'input'})
Note: This method will throw an error if it cannot find the node. If you don't
want this behavior (for example you wish to assert that it doesn't exist),
then use queryByLabelText
instead.
getByPlaceholderText(text: TextMatch): HTMLElement
This will search for all elements with a placeholder attribute and find one
that matches the given TextMatch
.
const inputNode = getByPlaceholderText('Username')
NOTE: a placeholder is not a good substitute for a label so you should
generally use getByLabelText
instead.
getByText(text: TextMatch): HTMLElement
This will search for all elements that have a text node with textContent
matching the given TextMatch
.
const aboutAnchorNode = getByText('about')
getByAltText(text: TextMatch): HTMLElement
This will return the element (normally an <img>
) that has the given alt
text. Note that it only supports elements which accept an alt
attribute:
<img>
,
<input>
,
and <area>
(intentionally excluding <applet>
as it's deprecated).
const incrediblesPosterImg = getByAltText(/incredibles.*poster$/i)
getByTestId(text: TextMatch): HTMLElement
A shortcut to container.querySelector(`[data-testid="${yourId}"]`)
(and it
also accepts a TextMatch
).
const usernameInputElement = getByTestId('username-input')
In the spirit of the guiding principles, it is
recommended to use this only after getByLabel
, getByPlaceholderText
or
getByText
don't work for your use case. Using data-testid attributes do
not resemble how your software is used and should be avoided if possible.
That said, they are way better than querying based on DOM structure.
Learn more about data-testid
s from the blog post
["Making your UI tests resilient to change"][data-testid-blog-post]
cleanup
Unmounts Preact trees that were mounted with render.
afterEach(cleanup)
test('renders into document', () => {
render(<div>)
})
Failing to call cleanup
when you've called render
could
result in a memory leak and tests which are not idempotent
(which can
lead to difficult to debug errors in your tests).
wait
Defined as:
function wait(
callback?: () => void,
options?: {
timeout?: number
interval?: number
},
): Promise<void>
When in need to wait for non-deterministic periods of time you can use wait
,
to wait for your expectations to pass. The wait
function is a small wrapper
around the
wait-for-expect
module.
Here's a simple example:
await wait(() => getByLabelText('username'))
getByLabelText('username').value = 'chucknorris'
This can be useful if you have a unit test that mocks API calls and you need
to wait for your mock promises to all resolve. This can also be useful when
(for example) you integration test your apollo-connected Preact components that
go a couple level deep, with queries fired up in consequent components.
The default callback
is a no-op function (used like await wait()
). This can
be helpful if you only need to wait for one tick of the event loop.
The default timeout
is 4500ms
which will keep you under
Jest's default timeout of 5000ms
.
The default interval
is 50ms
. However it will run your callback immediately
on the next tick of the event loop (in a setTimeout
) before starting the
intervals.
fireEvent(node: HTMLElement, event: Event)
Fire DOM events.
You can fire events directly to your DOM. You can render into the document using the
render utility.
import {cleanup, render, fireEvent} from 'preact-testing-library'
afterEach(cleanup)
test('clicks submit button', () => {
const spy = jest.fn()
const {getByText} = render(<button onClick={spy}>Submit</button>)
fireEvent(
getByText('Submit'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
)
expect(spy).toHaveBeenCalledTimes(1)
})
fireEvent[eventName](node: HTMLElement, eventProperties: Object)
Convenience methods for firing DOM events. Check out
dom-testing-library/src/events.js
for a full list as well as default eventProperties
.
const rightClick = {button: 2}
fireEvent.click(getElementByText('Submit'), rightClick)
debounceRenderingOff
Preact setState
is debounced, which means any call setState
, you have to use wait
or flushPromise
API to assert. However, there is a handy method for this debounceRenderingOff
.
You can invoke this debounceRenderingOff
to turn off the debounce feature of Preact.
test('testing different types of events with debounce off', () => {
debounceRenderingOff()
const {getByTestId, getByText, queryByText} = render(<MyForm />)
const checkBox = getByTestId('checkbox')
fireEvent.click(checkBox)
const textbox = getByTestId('textbox')
textbox.value = 'test value'
fireEvent.change(textbox)
expect(queryByText('No')).not.toBeInTheDOM()
expect(getByText('Yes')).toBeInTheDOM()
expect(getByText('test value')).toBeInTheDOM()
})
TextMatch
Several APIs accept a TextMatch
which can be a string
, regex
or a
function
which returns true
for a match and false
for a mismatch.
Here's an example
getByText('Hello World')
getByText('llo worl')
getByText('hello world')
getByText(/Hello W?oRlD/i)
getByText((content, element) => content.startsWith('Hello'))
getByText('Goodbye World')
getByText(/hello world/)
getByText((content, element) => {
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})
query
APIs
Each of the get
APIs listed in the render
section above have a
complimentary query
API. The get
APIs will throw errors if a proper node
cannot be found. This is normally the desired effect. However, if you want to
make an assertion that an element is not present in the DOM, then you can use
the query
API instead:
const submitButton = queryByText('submit')
expect(submitButton).toBeNull()
Feel free to contribute more!
FAQ
Which get method should I use?
Based on the Guiding Principles, your test should
resemble how your code (component, page, etc.) as much as possible. With this
in mind, we recommend this order of priority:
getByLabelText
: Only really good for form fields, but this is the number 1
method a user finds those elements, so it should be your top preference.getByPlaceholderText
: A placeholder is not a substitute for a label.
But if that's all you have, then it's better than alternatives.getByText
: Not useful for forms, but this is the number 1 method a user
finds other elements (like buttons to click), so it should be your top
preference for non-form elements.getByAltText
: If your element is one which supports alt
text
(img
, area
, and input
), then you can use this to find that element.getByTestId
: The user cannot see (or hear) these, so this is only
recommended for cases where you can't match by text or it doesn't make sense
(the text is dynamic).
Other than that, you can also use the container
to query the rendered
component as well (using the regular
querySelector
API).
Can I write unit tests with this library?
Definitely yes! You can write unit and integration tests with this library.
See below for more on how to mock dependencies (because this library
intentionally does NOT support shallow rendering) if you want to unit test a
high level component. The tests in this project show several examples of
unit testing with this library.
As you write your tests, keep in mind:
The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
What if my app is localized and I don't have access to the text in test?
This is fairly common. Our first bit of advice is to try to get the default
text used in your tests. That will make everything much easier (more than just
using this utility). If that's not possible, then you're probably best
to just stick with data-testid
s (which is not bad anyway).
What if I want to verify that an element does NOT exist?
You typically will get access to rendered elements using the getByTestId
utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the queryByTestId
utility which will return the element if found or null
if not.
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?
Definitely not. That said, a common reason people don't like the data-testid
attribute is they're concerned about shipping that to production. I'd suggest
that you probably want some simple E2E tests that run in production on occasion
to make certain that things are working smoothly. In that case the data-testid
attributes will be very useful. Even if you don't run these in production, you
may want to run some E2E tests that run on the same code you're about to ship to
production. In that case, the data-testid
attributes will be valuable there as
well.
All that said, if you really don't want to ship data-testid
attributes, then you
can use
this simple babel plugin
to remove them.
If you don't want to use them at all, then you can simply use regular DOM
methods and properties to query elements off your container.
const firstLiInDiv = container.querySelector('div li')
const allLisInDiv = container.querySelectorAll('div li')
const rootElement = container.firstChild
What if I’m iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?
You can make your selector just choose the one you want by including :nth-child in the selector.
const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')
Or you could include the index or an ID in your attribute:
<li data-testid={`item-${item.id}`}>{item.text}</li>
And then you could use the getByTestId
utility:
const items = [
]
const {getByTestId} = render()
const thirdItem = getByTestId(`item-${items[2].id}`)
What about enzyme is "bloated with complexity and features" and "encourage
poor testing practices"?
Most of the damaging features have to do with encouraging testing implementation
details. Primarily, these are
shallow rendering, APIs which
allow selecting rendered elements by component constructors, and APIs which
allow you to get and interact with component instances (and their
state/properties) (most of enzyme's wrapper APIs allow this).
The guiding principle for this library is:
The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
Because users can't directly interact with your app's component instances,
assert on their internal state or what components they render, or call their
internal methods, doing those things in your tests reduce the confidence they're
able to give you.
That's not to say that there's never a use case for doing those things, so they
should be possible to accomplish, just not the default and natural way to test
Preact components.
Guiding Principles
[The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
We try to only expose methods and utilities that encourage you to write tests
that closely resemble how your Preact components are used.
Utilities are included in this project based on the following guiding
principles:
- If it relates to rendering components, it deals with DOM nodes rather than
component instances, nor should it encourage dealing with component
instances.
- It should be generally useful for testing individual Preact components or
full Preact applications. While this library is focused on the actual dom,
utilities could be included even if they don't directly relate to the actual dom.
- Utility implementations and APIs should be simple and flexible.
At the end of the day, what we want is for this library to be pretty
light-weight, simple, and understandable.