Unit testing composables
Composables pull data logic out of components, so they are often the easiest place to test it.
renderDataCompose() from @data-client/vue/test runs a composable inside a component mounted
with DataClientPlugin and Fixtures.
It takes the same options and needs the same
setup as mountDataClient().
renderDataCompose()
renderDataCompose() is async; await it before reading result, which is whatever the
composable returned. useQuery() and useCache() return a
ComputedRef, so their data is in result.value:
import { renderDataCompose } from '@data-client/vue/test';
import { useQuery } from '@data-client/vue';
import { Article, ArticleResource } from './resources';
it('useQuery() returns cached data', async () => {
const { result, cleanup } = await renderDataCompose(
() => useQuery(Article, { id: 5 }),
{
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
},
);
expect(result.value?.title).toBe('hi ho');
cleanup();
});
Always call cleanup() at the end of a test; it unmounts the app and its managers.
useSuspense()
useSuspense() returns a Promise that resolves to a ComputedRef once the
data is available. await it once, then read .value; the ref stays reactive as the store changes.
import { useSuspense } from '@data-client/vue';
import { nextTick } from 'vue';
it('useSuspense() follows store updates', async () => {
const { result, controller, cleanup } = await renderDataCompose(
() => useSuspense(ArticleResource.get, { id: 5 }),
{
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
},
);
const article = await result;
expect(article.value.title).toBe('hi ho');
controller.setResponse(
ArticleResource.get,
{ id: 5 },
{ id: 5, title: 'updated', content: 'whatever' },
);
await nextTick();
expect(article.value.title).toBe('updated');
cleanup();
});
Add error: true to a fixture to test failures; the Promise then rejects with that error:
it('rejects on a failed fetch', async () => {
const { result, cleanup } = await renderDataCompose(
() => useSuspense(ArticleResource.get, { id: 5 }),
{
resolverFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { status: 404 },
error: true,
},
],
},
);
await expect(result).rejects.toMatchObject({ status: 404 });
cleanup();
});
Changing props
The composable receives props. Pass a reactive() object, and hand the composable a getter or
computed() built from it so it follows changes:
import { reactive } from 'vue';
it('follows prop changes', async () => {
const props = reactive({ id: 1 });
const { result, cleanup } = await renderDataCompose(
(props: { id: number }) => useQuery(Article, () => ({ id: props.id })),
{
props,
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 1 }],
response: { id: 1, title: 'First', content: '' },
},
{
endpoint: ArticleResource.get,
args: [{ id: 2 }],
response: { id: 2, title: 'Second', content: '' },
},
],
},
);
expect(result.value?.title).toBe('First');
props.id = 2;
await nextTick();
expect(result.value?.title).toBe('Second');
cleanup();
});
Mutations
The returned controller is the app's Controller. After a mutation,
await it and then nextTick() before checking the result:
it('push() adds to the list', async () => {
const { result, controller, cleanup } = await renderDataCompose(
() => useQuery(ArticleResource.getList.schema),
{
initialFixtures: [
{
endpoint: ArticleResource.getList,
args: [],
response: [{ id: 1, title: 'First', content: '' }],
},
],
resolverFixtures: [
{ endpoint: ArticleResource.getList.push, response: body => body },
],
},
);
expect(result.value).toHaveLength(1);
await controller.fetch(ArticleResource.getList.push, {
id: 2,
title: 'Second',
content: '',
});
await nextTick();
expect(result.value).toHaveLength(2);
cleanup();
});
API
renderDataCompose(composable, options?)
Resolves, once the composable has run, to the same controller, wrapper, cleanup() and
allSettled() as mountDataClient(), plus:
| Member | Description |
|---|---|
result | What the composable returned |
waitForNextUpdate() | Prefer await result or allSettled(); this waits for a pending Promise result with a 1 second cap |