12k
All articles

Testando Componentes Orientados a API no Storybook

Use MSW no Storybook 10 para testar componentes orientados por API em estados de carregando, erro, vazio e sucesso, e transformar stories em testes.

OpenReplay Team
OpenReplay Team
Testando Componentes Orientados a API no Storybook

Um componente que busca dados ficará preso em “Loading…” ou lançará um erro no Storybook porque não há um backend para responder à sua requisição — a solução é interceptar essa requisição na camada de rede com o Mock Service Worker, e não substituir o hook por um stub.

Se você já assistiu um componente ficar preso em “Loading…” no Storybook sem nenhuma mensagem no console para explicar o motivo, esta é a causa: não há servidor disponível para responder à requisição disparada na montagem do componente. Configure o mock uma única vez e todas as stories que você escrever receberão o mesmo tratamento automaticamente.

Este guia configura o msw-storybook-addon (que requer MSW 2.x) no Storybook 10, em seguida constrói um componente UserList com quatro stories (sucesso, carregamento, erro e vazio) e fecha o ciclo transformando esses estados mockados em testes de interação automatizados.

Principais Conclusões

  • Faça o mock na camada de rede para que um único conjunto de handlers MSW funcione sem alterações no Storybook, em testes unitários baseados em Node e na regressão visual do Chromatic.
  • No MSW v2, um handler de sucesso é http.get(url, () => HttpResponse.json(data)); não existe mais rest.get nem a assinatura de resolver (req, res, ctx) => res(ctx.json()).
  • Modele um estado de carregamento permanente aguardando delay('infinite'), um erro com HttpResponse.json(null, { status: 500 }) e um resultado vazio retornando HttpResponse.json([]).
  • Configure o addon uma única vez adicionando mswLoader ao array loaders em .storybook/preview, e sirva o worker definindo staticDirs na sua configuração principal.
  • Associe cada estado mockado a uma função play para que o estado seja verificado automaticamente. Uma story com uma função play torna-se um teste de componente.

Por que componentes que buscam dados falham no Storybook?

O Storybook renderiza componentes de forma isolada, sem shell de aplicação e sem servidor. Um “componente de aplicação” que chama fetch, useQuery ou Apollo na montagem dispara uma requisição que nada responde, fazendo com que ele fique preso indefinidamente no seu branch de carregamento ou lance um erro quando a promise é rejeitada. O instinto é substituir o hook por um stub: trocar useQuery por um mock que retorna dados fixos. Não faça isso. Criar um stub do hook acopla sua story a uma biblioteca de dados específica e à estrutura interna do componente, tornando-o inútil no momento em que você quiser executar o mesmo cenário em um teste Node.

Em vez disso, faça o mock na camada de rede. O MSW registra um service worker que intercepta as requisições de saída no navegador e retorna as respostas que você define, para que seu componente execute seu caminho real de busca de dados sem alterações. Os mesmos handlers são executados no Node via setupServer, o que os torna portáteis entre Storybook, Vitest e CI. Você cria o cenário uma vez; ele funciona em qualquer lugar onde o componente é executado.

Como configurar o stack de API mockada no Storybook?

Instale ambos os pacotes, gere o service worker, registre o loader e aponte o Storybook para o arquivo do worker. Esta configuração única segue o guia do Storybook para mockar requisições de rede.

npm install msw msw-storybook-addon --save-dev
npx msw init public/

npx msw init public/ grava o arquivo mockServiceWorker.js no seu diretório estático. Registre o addon globalmente adicionando mswLoader a loaders em .storybook/preview.ts. Os loaders são executados antes de uma story ser renderizada, razão pela qual o addon usa um loader em vez de um decorator:

import type { Preview } from '@storybook/react-vite';
import { initialize, mswLoader } from 'msw-storybook-addon';

initialize();

const preview: Preview = {
  loaders: [mswLoader],
};

export default preview;

Em seguida, sirva o worker gerado listando sua pasta public em staticDirs dentro de .storybook/main.ts. A antiga flag start-storybook -s public não existe mais no Storybook 10:

import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  staticDirs: ['../public'],
};

export default config;

A story de sucesso

Aqui está o componente em teste, um UserList que busca um array e renderiza um dos quatro branches de UI. Observe os atributos acessíveis role="status" e role="alert", que os testes consultarão posteriormente.

// UserList.tsx
import { useEffect, useState } from 'react';

type User = { id: number; name: string };
const endpoint = 'https://api.example.com/users';

export function UserList() {
  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    fetch(endpoint)
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => {
        setUsers(data);
        setStatus('success');
      })
      .catch(() => setStatus('error'));
  }, []);

  if (status === 'loading') return <p role="status">Loading…</p>;
  if (status === 'error') return <p role="alert">Something went wrong.</p>;
  if (users.length === 0) return <p>No users yet.</p>;

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Defina os handlers por story através de parameters.msw.handlers. No MSW v2, http.get substitui rest.get, e a classe HttpResponse substitui os utilitários ctx:

// UserList.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { http, HttpResponse, delay } from 'msw';
import { UserList } from './UserList';

const endpoint = 'https://api.example.com/users';

const meta = { component: UserList } satisfies Meta<typeof UserList>;
export default meta;
type Story = StoryObj<typeof meta>;

export const Success: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get(endpoint, () =>
          HttpResponse.json([
            { id: 1, name: 'Ada Lovelace' },
            { id: 2, name: 'Alan Turing' },
          ]),
        ),
      ],
    },
  },
};

Modelando cada estado

O caminho feliz é onde a maioria dos tutoriais para, e é a story menos interessante. O valor do mock na camada de rede está no fato de que a troca de um único handler produz todos os estados que seu componente pode assumir. Replays de sessão de UIs orientadas a API frequentemente revelam estados que os desenvolvedores nunca storyboardaram: um spinner que nunca resolve porque uma requisição travou, ou uma resposta vazia que renderiza como um layout quebrado em vez de um estado vazio. Storybook com MSW é onde você cria e verifica exatamente esses estados antes que eles cheguem à produção.

EstadoHandlerO que ele prova
Carregamentoawait delay('infinite') antes de responderA UI pendente renderiza e não pisca
ErroHttpResponse.json(null, { status: 500 })O branch de erro trata um 5xx
VazioHttpResponse.json([])O layout sem resultados é intencional, não quebrado

A função delay aceita um modo 'infinite' que mantém a requisição pendente indefinidamente — a forma confiável de congelar um componente no seu branch de carregamento. Importe delay do msw e use await dentro de um resolver assíncrono:

export const Loading: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get(endpoint, async () => {
          await delay('infinite');
          return HttpResponse.json([]);
        }),
      ],
    },
  },
};

export const Error: Story = {
  parameters: {
    msw: { handlers: [http.get(endpoint, () => HttpResponse.json(null, { status: 500 }))] },
  },
};

export const Empty: Story = {
  parameters: {
    msw: { handlers: [http.get(endpoint, () => HttpResponse.json([]))] },
  },
};

O MSW mocka GraphQL da mesma forma (graphql.query('AllUsers', () => HttpResponse.json({ data }))), portanto o padrão se aplica ao Apollo, urql e React Query sem alterações.

De visualização a testes

Uma story mockada que você apenas observa é documentação; adicione uma função play e ela se torna um teste de componente automatizável. Importe expect de storybook/test (o módulo atual, que substituiu o @storybook/test do Storybook 8) e faça asserções sobre o resultado renderizado:

import { expect } from 'storybook/test';

// em Success:
play: async ({ canvas }) => {
  await expect(await canvas.findByText('Ada Lovelace')).toBeInTheDocument();
},

// em Loading:
play: async ({ canvas }) => {
  await expect(canvas.getByRole('status')).toBeInTheDocument();
},

Para a story Loading com delay infinito, verifique que o spinner está presente. Não aguarde a resolução, pois a requisição fica pendente indefinidamente por design. Essas stories são executadas pelo addon Vitest, que as executa como testes de componente no navegador Chromium do Playwright a partir da UI do Storybook, do terminal ou do CI. Como os handlers MSW são agnósticos ao ambiente, os mesmos handlers de sucesso/erro/vazio suportam um teste Vitest independente via setupServer, e o Chromatic captura um snapshot de cada story para regressão visual.

Faça o mock na camada de rede, modele todos os quatro estados e associe uma função play a cada um: isso transforma uma pasta de stories em uma suíte de testes ativa que detecta os bugs de carregamento infinito e estado vazio quebrado antes que cheguem à produção. Comece adicionando as stories de carregamento, erro e vazio a um componente de aplicação existente hoje. Os handlers que você escrever lá são os mesmos que seus testes Vitest reutilizarão.

Perguntas Frequentes

Por que meu componente está preso em 'Loading…' no Storybook mesmo após instalar o msw-storybook-addon?

O componente está preso porque nenhum handler MSW corresponde à sua requisição ou o loader do addon não está configurado. Confirme que você adicionou mswLoader ao array loaders em .storybook/preview e chamou initialize(), que a pasta public foi gerada com npx msw init e listada em staticDirs, e que um handler em parameters.msw.handlers corresponde exatamente à URL e ao método da requisição. Uma incompatibilidade de URL deixa a requisição sem tratamento e o componente pendente indefinidamente.

Qual é a diferença entre mockar na camada de rede com MSW e criar um stub do hook fetch?

O mock na camada de rede com MSW intercepta a requisição de saída real e retorna uma resposta, para que o componente execute seu caminho real de busca de dados sem alterações e os mesmos handlers funcionem no navegador, no Node via setupServer e no Chromatic. Criar um stub do hook substitui useQuery ou fetch por dados fixos, o que acopla a story a uma biblioteca de dados específica e à estrutura interna do componente, e não pode ser reutilizado em um teste Node.

O msw-storybook-addon funciona com handlers do MSW v1 como rest.get e res(ctx.json())?

Não. Desde a versão 2.0.0, o addon requer MSW 2.0.0 ou superior, e o MSW v2 removeu o namespace rest e a assinatura de resolver res(ctx.json()). Reescreva os handlers usando http.get e a classe HttpResponse, por exemplo http.get(url, () => HttpResponse.json(data)). O código do MSW v1 não funcionará com o addon atual e deve ser migrado usando o guia oficial de migração do MSW de 1.x para 2.x.

Por que minha story de carregamento com delay infinito trava quando executada como teste?

Uma story que usa await delay('infinite') mantém sua requisição pendente indefinidamente por design, portanto uma função play que aguarda a UI resolvida nunca é concluída. Verifique apenas que a UI pendente está presente, por exemplo que o spinner com role status é renderizado, em vez de aguardar a resolução. Se um teste Node ou Vitest precisar ser concluído de forma limpa, use um delay finito como delay(1000) em vez do modo infinito para esse cenário.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.