Solid Query works by means of primitives - either the ones we offer or custom ones that wrap around them.
These primitives read the QueryClient from QueryClientProvider, so they have to run under it. Writing unit tests for them can be done by means of renderHook from Solid Testing Library.
Install this by running:
npm install @solidjs/testing-library --save-devOnce installed, a simple test can be written. Given the following custom primitive:
export function useCustomQuery() {
return useQuery(() => ({ queryKey: ['customQuery'], queryFn: () => 'Hello' }))
}We can write a test for this as follows:
import { renderHook, waitFor } from '@solidjs/testing-library'
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
import type { ParentProps } from 'solid-js'
const queryClient = new QueryClient()
const wrapper = (props: ParentProps) => (
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
)
const { result } = renderHook(() => useCustomQuery(), { wrapper })
await waitFor(() => expect(result.isSuccess).toBe(true))
expect(result.data).toEqual('Hello')Note that we provide a custom wrapper that builds the QueryClient and QueryClientProvider. This helps to ensure that our test is completely isolated from any other tests.
It is possible to write this wrapper only once, but if so we need to ensure that the QueryClient gets cleared before every test, and that tests don't run in parallel otherwise one test will influence the results of others.
result is the reactive object that useQuery returns, not a snapshot, so read its properties after waiting instead of destructuring them up front. A destructured value keeps the value it had when it was read.
waitFor retries the assertion until it passes, which works for query functions that wait on real requests. When the query function waits on a timer, you can use fake timers and advance them instead, for example with vi.advanceTimersByTimeAsync in Vitest. Restore real timers in afterEach, so they are restored even when an assertion fails:
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should resolve the delayed query', async () => {
const { result } = renderHook(
() =>
useQuery(() => ({
queryKey: ['delayed'],
queryFn: () =>
new Promise((resolve) => setTimeout(() => resolve('Hello'), 1000)),
})),
{ wrapper },
)
await vi.advanceTimersByTimeAsync(1000)
expect(result.data).toEqual('Hello')
})For more complex primitives, it can be easier to test the component that uses them instead, by rendering it with render from Solid Testing Library under the same QueryClientProvider and asserting on what it renders.
The library defaults to three retries with exponential backoff, which means that your tests are likely to timeout if you want to test an erroneous query. The easiest way to turn retries off is via the QueryClient that you pass to QueryClientProvider. Let's extend the above example:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ✅ turns retries off
retry: false,
},
},
})
const wrapper = (props: ParentProps) => (
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
)This will set the defaults for all queries in the component tree to "no retries". It is important to know that this will only work if your actual useQuery has no explicit retries set. If you have a query that wants 5 retries, this will still take precedence, because defaults are only taken as a fallback.
If you use Jest, you can set the gcTime to Infinity to prevent "Jest did not exit one second after the test run completed" error message. This is the default behavior on the server, and is only necessary to set if you are explicitly setting a gcTime.