Testing Remix
Loading "Intro to Testing Remix"
Run locally for transcripts
When our components are rendered, they are using a
context provider
from Remix. When we try to render our components in our test, things like
useLoaderData
and <Link>
will expect to have that context available. The
tricky bit with Remix is that so much of what you're doing relies on the routing
layer which often involves loaders and actions.So, the Remix team is working on a great solution for this called
createRemixStub
which allows you to create a mini-Remix app that you can
render in your test and have all the routes you need for testing the component:import { useLoaderData } from '@remix-run/react'
import { db } from '#app/utils/db.server'
export async function loader() {
return json({ count: await db.getCount() })
}
export async function action() {
await db.incrementCount()
return redirect('/counter')
}
export default function Counter() {
const data = useLoaderData<typeof loader>()
return (
<Form method="post">
<button type="submit">Count: {data.count}</button>
</Form>
)
}
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { json, redirect } from '@remix-run/node'
import { createRemixStub } from '@remix-run/testing'
import Counter from './counter'
test('counter increments when clicked', async () => {
let count = 0
const App = createRemixStub([
{
path: '/counter',
Component: Counter,
loader: () => json({ count }),
action: () => {
count = count + 1
return redirect('/counter')
},
},
])
await render(<App initialEntries={['/counter']} />)
const button = await screen.findByRole('button', { name: /count:/i })
expect(button).toHaveTextContent('Count: 0')
await userEvent.click(button)
expect(button).toHaveTextContent('Count: 1')
})
It's pretty neat that we can test the UI component with some mocked backend
logic, however, I'm often interested in testing the component holistically, so
we can actually import and use the original
action
and loader
from the route
as well:import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createRemixStub } from '@remix-run/testing'
// ๐ import the loader and action
import Counter, { loader, action } from './counter'
test('counter increments when clicked', async () => {
const App = createRemixStub([
{
path: '/counter',
Component: Counter,
// ๐ use the original loader and action
loader,
action,
},
])
await render(<App initialEntries={['/counter']} />)
const button = await screen.findByRole('button', { name: /count:/i })
expect(button).toHaveTextContent('Count: 0')
await userEvent.click(button)
expect(button).toHaveTextContent('Count: 1')
})
The trick here is to make sure you've got a database working in your test, which
we'll cover more in the next exercise.
Stubs
A stub is kind of like a mock in that it's a fake version of something. However,
a stub is a bit more lightweight and doesn't have the same assertions that a
mock does. A stub is just a function that returns a value. You can use a stub to
replace a function that you don't want to run in your test, or to return a
specific value that you want to test against. But it's not like a mock function
that you can say
expect(fn).toHaveBeenCalledTimes(1)
for example.In our case, the Remix stub is just a fake version of Remix's context providers
so our components can render without errors and with preset values we specify.