37
loading...
This website collects cookies to deliver better user experience
// composeStories from `@storybook/testing-react`
const Story = composeStories(stories)
const { queryClient } = renderStory(<Story.FancyButton />)
// ...wait for query client state, assert state, etc
I prefer to give each test its own QueryClientProvider and create a new QueryClient for each test. That way, tests are completely isolated from each other. A different approach might be to clear the cache after each test, but I like to keep shared state between tests as minimal as possible. Otherwise, you might get unexpected and flaky results if you run your tests in parallel.
- TkDodo on Testing React Query
import * as React from 'react'
// local util module to wrap test utils like React Testing
// Library (RTL) and @storybook/testing-react
import * as Test from 'test'
import * as stories from './index.stories'
const Story = Test.composeStories(stories)
test("FancyButton shows an alert for failures", () => {
Test.renderStory(<Story.Failure />)
Test.user.click(Test.screen.getByText(/do stuff/i))
await Test.findByText(/uh oh!/i)
})
*.spec.tsx
files have been very concise and declarative. This is because all the setup is in *.stories.tsx
files. Tests just become expressions of how I'm testing the stories, as a user, in the browser.queryClient
instance to leverage patterns like:await Test.waitFor(() => {
expect(queryClient.isFetching()).toEq(0)
})
queryClient
instance.queryClient
instance is easily accessible in each test.queryClient
feels like "The Testing Library Way".const { rerender } = Test.render(<FancyButton />)
const { queryClient } = Test.render(<FancyButton />)
queryClient
is unique to this particular invocation of Test.render
.queryClient
nor the QueryClientProvider
at the individual story level for the same reasons I wouldn't instantiate it in each test: too much annoying boilerplate that makes writing stories less fun. So that's out. We need some kind of "do this for every test" lever.render
function that wraps the component under test the same way your app is globally wrapped by some combination of providers. We'll borrow this notion of "all the providers" but skip the custom render wrapper and instead use it for a Storybook decorator. Since we'll want control of our queryClient
, we'll parameterize it for a root provider.// ./test/index.tsx
import React from "react";
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "react-query";
import { ChakraProvider } from "@chakra-ui/react";
export const makeQueryClient = () =>
new QueryClient({
defaultOptions: { queries: { retry: false } }
});
type Props = { queryClient?: QueryClient };
export const AllTheProviders: React.FC<Props> = ({
queryClient = makeQueryClient(),
children
}) => {
return (
<QueryClientProvider client={queryClient}>
<ChakraProvider>{children}</ChakraProvider>
</QueryClientProvider>
);
};
AllTheProviders
.// .storybook/main-decorator.tsx
import * as React from "react";
import { AllTheProviders } from "../test";
export const MainDecorator: DecoratorFn = (
Story,
options
) => {
return (
<AllTheProviders queryClient={options.args.queryClient}>
<Story {...options} />
</AllTheProviders>
);
};
options.args.queryClient
is still nullable, but allows us to pass a query client to the component results of composeStories
.preview.js
.// .storybook/preview.js
import { MainDecorator } from './main-decorator'
//...
export const decorators = [AllTheProviders]
composeStories
from @storybook/testing-react
, but we need a custom render function that adds queryClient
to the return value of render
from React Testing Library.export const renderStory = (
ui: React.ReactElement<{ queryClient?: QueryClient }>,
options: RenderOptions = {}
) => {
const queryClient: QueryClient =
ui.props.queryClient ?? makeQueryClient();
const clonedUi = React.cloneElement(ui, { queryClient });
return { ...render(clonedUi, options), queryClient };
};
React.cloneElement
to modify the already-invoked component function so we can pass a queryClient
from a different scope. If the ui
component was already called with a queryClient
, that will be reused thanks to our nullish coalescing operator ??
. Now in our tests we can access the queryClient
as a result of our render
call.const { queryClient } = Test.renderStory(<Story.FancyButton />)
const queryClient = makeQueryClient()
const invalidateQueriesSpy =
jest.spyOn(queryClient, 'invalidateQueries');
Test.render(<Story.Success queryClient={queryClient} />)
Test.user.click(Test.screen.getByText(/do stuff/i))
expect(queryClient.invalidateQueries)
.toHaveBeenCalledWith("user-profile")
ui.props.queryClient
check comes into play.