TanStack
Guides & Concepts

Testing

Vue Query works by means of composables - either the ones we offer or custom ones that wrap around them.

These composables need the QueryClient that VueQueryPlugin provides, so they have to run inside the setup of a component that has the plugin installed. Writing unit tests for them can be done by means of Vue Test Utils.

Setup

Following the Testing Composables approach of the Vue docs, we write a withSetup helper that mounts a component, runs the composable in its setup and installs VueQueryPlugin with a QueryClient. The helper differs between Vue 3 and Vue 2, because they use different major versions of Vue Test Utils.

Vue 3

Install Vue Test Utils 2 by running:

shell
npm install @vue/test-utils --save-dev
ts
import { defineComponent } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'

function withSetup<T>(composable: () => T, queryClient = new QueryClient()) {
  let result!: T
  const wrapper = mount(
    defineComponent({
      setup() {
        result = composable()
        return () => null
      },
    }),
    { global: { plugins: [[VueQueryPlugin, { queryClient }]] } },
  )
  return [result, wrapper] as const
}

Vue 2

Vue Test Utils 1 does not export flushPromises, so install the flush-promises package along with it:

shell
npm install @vue/test-utils@1 flush-promises --save-dev
ts
import { defineComponent } from 'vue'
import { createLocalVue, mount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'

function withSetup<T>(composable: () => T, queryClient = new QueryClient()) {
  let result!: T
  const localVue = createLocalVue()
  localVue.use(VueQueryPlugin, { queryClient })
  const wrapper = mount(
    defineComponent({
      setup() {
        result = composable()
        return () => null
      },
    }),
    { localVue },
  )
  return [result, wrapper] as const
}

With Vue 2.6, also call localVue.use(VueCompositionApi) before installing VueQueryPlugin, and import VueCompositionApi and defineComponent from @vue/composition-api.

Our First Test

Once set up, a simple test can be written. Given the following custom composable:

ts
export function useCustomComposable() {
  return useQuery({ queryKey: ['customComposable'], queryFn: () => 'Hello' })
}

We can write a test for this as follows:

ts
const [result, wrapper] = withSetup(() => useCustomComposable())

await flushPromises()

expect(result.isSuccess.value).toBe(true)
expect(result.data.value).toEqual('Hello')

wrapper.unmount()

With Vue Test Utils 1, call wrapper.destroy() instead of wrapper.unmount().

Note that withSetup creates a new QueryClient for every call. This helps to ensure that our test is completely isolated from any other tests. Unmounting the wrapper at the end of the test removes the query observers that the composable created.

It is possible to share a single QueryClient between tests, 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.

flushPromises is enough here because the query function resolves right away. When the query function waits on a timer, use fake timers and advance them, for example with vi.advanceTimersByTimeAsync in Vitest. Restore real timers in afterEach, so they are restored even when an assertion fails:

ts
beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

it('should resolve the delayed query', async () => {
  const [result, wrapper] = withSetup(() =>
    useQuery({
      queryKey: ['delayed'],
      queryFn: () =>
        new Promise((resolve) => setTimeout(() => resolve('Hello'), 1000)),
    }),
  )

  await vi.advanceTimersByTimeAsync(1000)

  expect(result.data.value).toEqual('Hello')

  wrapper.unmount()
})

When the query function waits on a real request, wait until the assertion passes instead, for example with vi.waitFor:

ts
await vi.waitFor(() => expect(result.isSuccess.value).toBe(true))

For more complex composables, it can be easier to test the component that uses them instead, by mounting it with the same plugin setup and asserting on what it renders.

Turn off retries

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 VueQueryPlugin. Let's extend the above example:

ts
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ turns retries off
      retry: false,
    },
  },
})

const [result, wrapper] = withSetup(() => useCustomComposable(), queryClient)

This will set the defaults for all queries in the app 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.

Set gcTime to Infinity with Jest

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.