Pular para o conteúdo principal

Testes unitários de componentes

aviso

Tenha cuidado ao usar jest.mock em módulos como o Reactive Data Client. Eliminar exports esperados pode levar a erros difíceis de rastrear, como TypeError: Class extends value undefined is not a function or null.

Em vez disso, carregue as respostas com Fixtures.

@data-client/vue/test monta componentes com o DataClientPlugin, um limite <Suspense> e Fixtures, de modo que os testes possam verificar o que um componente renderiza sem um ciclo de fetch de rede. Para composables isolados, veja Testes unitários de composables.

Configuração​

Os utilitários de teste são construídos sobre o Vue Test Utils, então instale-o junto com seu test runner. O Jest precisa do ambiente jsdom.

npm install --saveDev @vue/test-utils jest-environment-jsdom

mountDataClient()​

initialFixtures preenchem o store antes da primeira renderização, de modo que um componente que faz await em useSuspense() renderize seus dados imediatamente.

import { mountDataClient } from '@data-client/vue/test';
import { useSuspense } from '@data-client/vue';
import { defineComponent, h } from 'vue';
import { ArticleResource } from './resources';
import { flushUntil } from './flushUntil';

const ArticleDetail = defineComponent({
props: { id: { type: Number, required: true } },
async setup(props) {
// a getter so the fetch follows props.id
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));
return () => h('h3', article.value.title);
},
});

it('renders the article', async () => {
const { wrapper, cleanup } = mountDataClient(ArticleDetail, {
props: { id: 5 },
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
});

await flushUntil(() => wrapper.find('h3').exists());
expect(wrapper.find('h3').text()).toBe('hi ho');

cleanup();
});

Sempre chame cleanup() ao final de um teste; ele desmonta o app e seus managers.

Aguardando renderizações​

setup() assíncrono e Suspense são resolvidos ao longo de várias microtasks e ticks, então aguarde o que o teste espera em vez de um número fixo de ticks:

flushUntil.ts
import { nextTick } from 'vue';

export async function flushUntil(predicate: () => boolean, tries = 100) {
for (let i = 0; i < tries; i++) {
if (predicate()) return;
await nextTick();
await new Promise(resolve => setTimeout(resolve, 0));
}
throw new Error('flushUntil: condition never became true');
}

Estado de carregamento​

Enquanto um componente está suspenso, mountDataClient() renderiza um elemento com data-testid="suspense-fallback". Use resolverFixtures para responder aos fetches que o store ainda não tem:

it('shows the fallback until the fetch resolves', async () => {
const { wrapper, cleanup } = mountDataClient(ArticleDetail, {
props: { id: 5 },
resolverFixtures: [
{
endpoint: ArticleResource.get,
response: ({ id }) => ({ id, title: `Article ${id}`, content: '' }),
},
],
});

expect(wrapper.find('[data-testid="suspense-fallback"]').exists()).toBe(true);

await flushUntil(() => wrapper.find('h3').exists());
expect(wrapper.find('h3').text()).toBe('Article 5');

cleanup();
});

O response de um interceptor recebe os argumentos do endpoint, então uma única fixture pode responder a todo id. Adicione error: true a uma fixture para fazê-la rejeitar; veja Fixtures para todos os formatos de fixture.

Alterando props​

Passe um objeto reactive() como props e depois altere seus membros para mudar as props do componente:

import { reactive } from 'vue';

it('fetches the new article when id changes', async () => {
const props = reactive({ id: 1 });
const { wrapper, cleanup } = mountDataClient(ArticleDetail, {
props,
resolverFixtures: [
{
endpoint: ArticleResource.get,
response: ({ id }) => ({ id, title: `Article ${id}`, content: '' }),
},
],
});

await flushUntil(() => wrapper.text() === 'Article 1');

props.id = 2;
await flushUntil(() => wrapper.text() === 'Article 2');

cleanup();
});

Um setup() assíncrono é executado uma vez por instância do componente, então passe argumentos derivados de props como um getter (() => ({ id: props.id })) ou computed(). Um simples { id: props.id } é lido uma única vez e não acompanha as mudanças de props.

Mutações​

O controller retornado é o Controller do app. Execute mutações ou setResponse() com ele e depois aguarde o componente ser atualizado:

it('re-renders when the store changes', async () => {
const { wrapper, controller, cleanup } = mountDataClient(ArticleDetail, {
props: { id: 5 },
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
});
await flushUntil(() => wrapper.find('h3').exists());

controller.setResponse(
ArticleResource.get,
{ id: 5 },
{ id: 5, title: 'updated', content: 'whatever' },
);

await flushUntil(() => wrapper.find('h3').text() === 'updated');
cleanup();
});

API​

mountDataClient(component, options?)​

Retorna

MembroDescrição
wrapperVueWrapper do Vue Test Utils da árvore montada
controllerO Controller do app
appA instância do app Vue
cleanup()Desmonta e interrompe os managers
allSettled()Resolve quando todo fetch em andamento é concluído, incluindo fetches que uma mudança de prop acabou de iniciar, e o componente é renderizado novamente

Options​

mountDataClient() e renderDataCompose() recebem as mesmas opções.

interface RenderDataClientOptions<P = any> {
/** Props for the component; pass a reactive() object to change them */
props?: Reactive<P>;
/** Responses in the store before the first render */
initialFixtures?: readonly Fixture[];
/** Answer fetches made during the test */
resolverFixtures?: readonly (Fixture | Interceptor)[];
/** Initial `this` for interceptors */
getInitialInterceptorData?: () => any;
/** Replace the default NetworkManager and SubscriptionManager */
managers?: Manager[];
/** Replace the state built from initialFixtures */
initialState?: State<unknown>;
gcPolicy?: GCInterface;
/** Component rendered around the tested component (receives the same props) */
wrapper?: Component;
}

mockInitialState() constrói o initialState a partir de fixtures, para montar com a sua própria configuração do DataClientPlugin.