- React Fundamentals
- React Hooks
- Advanced React Hooks
- Advanced React Patterns
- React Performance
- Testing React Apps
- React Suspense
- Build an Epic React App
When I need to check things loosely (like to be called with an object with certain structure, but not exact), check the jest -> expect docs page, check the asymetric matchers.
In React v18, you're required to wrap all your interactions in
act
, but RTL does it
automatically for you.
.click()
(out of RTL) is not exactly what it happens in the browser, it's more
precise to use dispatchEvent(event: Event)
React needs changes to components wrapped in act()
. RTL does it for you, but
only for any of your code that is running within the React callstack (like click
events where React calls into your event handler code which updates the
component), but it cannot handle this for any code running outside of it's own
callstack (like asynchronous code that runs as a result of a resolved promise
you are managing or if you're using jest fake timers). With those kinds of
situations you typically need to wrap that in act(...) or async act(...)
yourself. BUT, React Testing Library has async utilities that are wrapped in act
automatically!
If you're still experiencing the act warning, then the most likely reason is something is happening after your test completes for which you should be waiting (e.g. wait for something to appear or disappear)
You can also see the warning (and you have to manually add act()
) when:
- When using
jest.useFakeTimers()
- When testing custom hooks
(
import { renderHook } from '@testing-library/react'
), when you call functions returned from your hooks which result in state updates - When using
useImperativeHandle
(you only run into this if you're calling methods directly on a component which do internal state updates and you're outside of React's callstack)
const {container} = render(<MyComponent />)
container
is just the div
RTL creates for you when you render and where it
renders your component nested on it.
Use RTL custom jest matchers instead of Jest ones for better error messages.
If you need to give a name to the object returned by render()
use one of these
const view = render(<MyComp />)
const utils = render(<MyComp />)
Check within()
functionality, to not query the whole document.body
const messages = document.getElementById('messages')
const helloMessage = within(messages).getByText('hello')
!! Checkout the Dev Tools > Elements > Accessibility tab, where you can see the role and name of elements.
Testing-Playground Check which RTL queries are best to get elements in this sandbox.
Use userEvent
instead of fireEvent
to do what the user would do. For
example, if the user click, a lot of events will happen: mouseOver, mouseDown,
mouseUp, click, ... userEvent
takes care to replicate all this when you do
await userEvent.click()
. It returns promises so remember to use await
.
Use Testing Playground DevTool extension
screen.debug()
to check what the UI looks like after render
password inputs don't have a role for seccurity reasons, get them with
getByLabelText()
Use jest.fn()
and toHaveBeenCalledWith()
Use faker
to generate input which exact value is irrelevant to communicate
that is irrelevant to whoever reads the test.
Use @jackfranklin/test-data-bot
to automate more data creation.
Elaborate
Use MSW (Mock Service Worker) for that, intercepts instead of being a real server, more convenient as you don't have to worry about ports and you can test fetching from other domains.
waitForElementToBeRemoved
RTL util
RTL name
in getByRole can be an input label, the content of a button, or the
aria-label
attribute of an element.
toBeVisible
is like toBeInTheDocument
with extra checks like
- display != none
- opacity greater than 0 etc.
msw
was actually built for having a mock server during development, it's
convenient to have one so you can work with APIs that are not done yet. If you
do so, make sure you share your server handlers (the mocks) so you use the same
ones for development and testing.
Remember you can implement logic in your server mock, not only a response. So you can return the normal response, but if some parameter is missing or it's a certain value, then return an error, etc.
.toMatchInlineSnapshot()
with no arguments will be auto-populated the first
time the test runs. Think whether you want to snapshot a whole div or only its
content.
Use server.use
and server.resetHandlers()
to add specific ones for a test
(like server unexpectedly failing) that would not make sense to have in the
handlers used for development.
Sometimes you have to use a polyfill or monkey patch some browser functionality
(e.g.: window.resizeTo
and window.matchMedia
), as what RTL uses instead of a
real browser (jsdom) doesn't support everything a browser does.
Mock modules: jest.mock('./path')
to mock a module, all the exports are now
jest mock functions so you can .mockImplementation()
,
toHaveBeenCalledTimes()
, etc.
Use requireActual()
to mock only parts of the module
jest.mock('../math', () => {
const actualMath = jest.requireActual('../math')
return {
...actualMath,
subtract: jest.fn(),
}
})
You can use jest.doMock
and jest.dontMock
to opt-out of the hoisting
behaviour.
Useful util to resolve/reject a promis when you want
function deferred() {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return {promise, resolve, reject}
}
💰 Here's an example of how you use this:
const {promise, resolve, reject} = deferred()
promise.then(() => {
/* do something */
})
// do other setup stuff and assert on the pending state
resolve()
await promise
// assert on the resolved state
act()
is needed when there is a state update that React was not expecting
(e.g. get location promise resolves, and the calls the callback which updates
the state). Wrapping that promis resolve in act ensures that React will flush
all the state changes so we are not left on a pending state when proceed with
the next assertions.
Careful defining the mockImplementations out of tests, as there are automatic cleanings happening before each test.
Another way to test async stuff, apart of the deferred
util above, when
mocking a hook, is to use something async inside like useState
and asign the
setX
method to a variable outside the mock implementation, that way the state
can be changed when desired to move from the pending state to the resolved one
and assert both states.
Use the wrapper
option with the provider when using components which use
context, so you can re-render them without re-rendering the provider:
function Wrapper({children}) {
return <ContextProvider>{children}</ContextProvider>
}
const {rerender} = render(<ComponentToTest />, {wrapper: Wrapper})
rerender(<ComponentToTest newProp={true} />)
You can create a custom render function like we did in Atom Learning so you
don't have to use wrapper
in each test.
To set it up, read this
To access your src files like they are modules (not using ../../myFile
etc.)
you can go to jest.config.js
and in the option moduleDirectories add your
src
moduleDirectories: ['node_modules', path.join(__dirname, 'src')]
You shouldn't test custom hooks, you should test them via testing a component which uses them.
However, if you are building a reusable hook, or you have a library of hooks or something like that, it might be a good idea to test them. Still, your hook is going to be used inside of a component, so just make a silly component that uses it to test it.
When that's too complicated, make your silly component like this
let result
function TestComponent(props) {
result = useCustomHook(props)
return null
}
// interact with and assert on results here
Even better, use renderHook
from @testing-library/react
, which is like the
above setup
In addition to {result}
it returns a few more things
- Utility to "rerender" the component that's rendering the hook (to test effect dependency changes for example)
- Utility to "unmount" the component that's rendering the hook (to test effect cleanup functions for example)
- Several async utilities to wait an unspecified amount of time (to test async logic)
When I use act
I indicate: I expect a change of state to happen, flush all the
changes so the component is stable, and then I continue asserting.
Remember userEvent
utils wrap everything in act
calls.