composable 的单元测试
composable 把数据逻辑从组件中抽离出来,因此它往往是测试这些逻辑最容易的地方。
@data-client/vue/test 提供的 renderDataCompose() 会在一个挂载了
DataClientPlugin 和 Fixtures 的组件中运行 composable。它接受与 mountDataClient() 相同的 options,也需要相同的
setup。
renderDataCompose()
renderDataCompose() 是异步的,在读取 result 之前要先 await 它;后者就是
composable 的返回值。useQuery() 和 useCache() 返回的是
ComputedRef,因此它们的数据位于 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();
});
务必在测试结束时调用 cleanup();它会卸载应用及其 manager。
useSuspense()
useSuspense() 返回一个 Promise,数据可用后它会 resolve 为一个 ComputedRef。只需 await 一次,然后读取 .value;随着 store 的变化,这个 ref 会保持响应式。
import { renderDataCompose } from '@data-client/vue/test';
import { useSuspense } from '@data-client/vue';
import { nextTick } from 'vue';
import { ArticleResource } from './resources';
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();
});
在 fixture 中添加 error: true 来测试失败情况;此时 Promise 会以该错误 reject:
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();
});
修改 props
composable 会接收 props。传入一个 reactive() 对象,并把基于它构建的 getter 或
computed() 交给 composable,使其能跟随变化:
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();
});
变更
返回的 controller 就是应用的 Controller。执行变更之后,先 await 它,再 nextTick(),然后检查结果:
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?)
composable 运行完毕后,它会 resolve 为与 mountDataClient() 相同的 controller、wrapper、cleanup() 和
allSettled(),另外还有:
| 成员 | 说明 |
|---|---|
result | composable 的返回值 |
waitForNextUpdate() | 已弃用。 1 秒后会静默放弃,因此测试可能在仍处于挂起状态时就通过了。对于 Promise 类型的 result,请使用 await result;发生变化后请使用 await allSettled() |