Welcome to the EpicWeb.dev Workshop app!

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

Testing Remix

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.