Picasso: como testar uma biblioteca de componentes
Publicados: 2022-03-11Uma nova versão do sistema de design da Toptal foi lançada recentemente, o que exigiu que fizéssemos alterações em quase todos os componentes do Picasso, nossa biblioteca de componentes interna. Nossa equipe enfrentou um desafio: como garantir que as regressões não aconteçam?
A resposta curta é, sem surpresa, testes. Muitos testes.
Não revisaremos os aspectos teóricos dos testes nem discutiremos os diferentes tipos de testes, sua utilidade ou explicaremos por que você deve testar seu código em primeiro lugar. Nosso blog e outros já abordaram esses tópicos. Em vez disso, vamos nos concentrar apenas nos aspectos práticos dos testes.
Continue lendo para saber como os desenvolvedores da Toptal escrevem testes. Nosso repositório é público, então usamos exemplos do mundo real. Não há abstrações ou simplificações.
Pirâmide de teste
Não temos uma pirâmide de testes definida, por si só, mas se tivéssemos ficaria assim:
A pirâmide de testes da Toptal ilustra os testes que enfatizamos.
Testes de unidade
Os testes de unidade são simples de escrever e fáceis de executar. Se você tem muito pouco tempo para escrever testes, eles devem ser sua primeira escolha.
No entanto, eles não são perfeitos. Independentemente de qual biblioteca de teste você escolher (Jest and React Testing Library [RTL] no nosso caso), ela não terá um DOM real e não permitirá que você verifique a funcionalidade em diferentes navegadores, mas permitirá que você retire eliminar a complexidade e testar os blocos de construção simples de sua biblioteca.
Os testes de unidade não apenas agregam valor testando o comportamento do código, mas também verificando a testabilidade geral do código. Se você não pode escrever testes de unidade facilmente, é provável que você tenha um código ruim.
Testes de regressão visual
Mesmo que você tenha 100% de cobertura de teste de unidade, isso não significa que os componentes tenham uma boa aparência em todos os dispositivos e navegadores.
As regressões visuais são particularmente difíceis de detectar com testes manuais. Por exemplo, se o rótulo de um botão for movido em 1 px, um engenheiro de controle de qualidade perceberá? Felizmente, existem muitas soluções para este problema de visibilidade limitada. Você pode optar por soluções completas de nível empresarial, como LambdaTest ou Mabl. Você pode incorporar plugins, como Percy, em seus testes existentes, bem como soluções DIY de Loki ou Storybook (que é o que usamos antes do Picasso). Todos eles têm desvantagens: alguns são muito caros, enquanto outros têm uma curva de aprendizado íngreme ou exigem muita manutenção.
Happo para o resgate! É um concorrente direto do Percy, mas é muito mais barato, suporta mais navegadores e é mais fácil de usar. Outro grande ponto de venda? Ele suporta a integração do Cypress, o que foi importante porque queríamos deixar de usar o Storybook para testes visuais. Nos encontramos em situações em que precisávamos criar histórias apenas para garantir a cobertura visual do teste, não porque precisávamos documentar esse caso de uso. Isso poluiu nossos documentos e os tornou mais difíceis de entender. Queríamos isolar o teste visual da documentação visual.
Testes de integração
Mesmo que dois componentes tenham testes unitários e visuais, isso não é garantia de que funcionarão juntos. Por exemplo, encontramos um bug em que uma dica de ferramenta não abre quando usada em um item suspenso, mas funciona bem quando usada sozinha.
Para garantir que os componentes se integrem bem, usamos o recurso de teste de componentes experimentais do Cypress. No início, ficamos insatisfeitos com o baixo desempenho, mas conseguimos melhorá-lo com uma configuração de webpack personalizada. O resultado? Conseguimos usar a excelente API do Cypress para escrever testes de desempenho que garantem que nossos componentes funcionem bem juntos.
Aplicando a Pirâmide de Testes
Como é tudo isso na vida real? Vamos testar o componente Acordeão!
Seu primeiro instinto pode ser abrir seu editor e começar a escrever código. Meu conselho? Passe algum tempo entendendo todos os recursos do componente e anote quais casos de teste você deseja cobrir.
O que Testar?
Aqui está um detalhamento dos casos que nossos testes devem cobrir:
- Estados – Acordeões podem ser expandidos e recolhidos, seu estado padrão pode ser configurado e esse recurso pode ser desabilitado
- Estilos – Acordeões podem ter variações de borda
- Conteúdo – Podem se integrar com outras unidades da biblioteca
- Personalização – O componente pode ter seus estilos substituídos e pode ter ícones de expansão personalizados
- Callbacks – Sempre que o estado muda, um callback pode ser invocado
Como testar?
Agora que sabemos o que temos que testar, vamos considerar como fazê-lo. Temos três opções da nossa pirâmide de testes. Queremos alcançar a cobertura máxima com sobreposição mínima entre as seções da pirâmide. Qual é a melhor maneira de testar cada caso de teste?
- Estados – Testes de unidade podem nos ajudar a avaliar se os estados mudam de acordo, mas também precisamos de testes visuais para garantir que o componente seja renderizado corretamente em cada estado
- Estilos – Testes visuais devem ser suficientes para detectar regressões das diferentes variantes
- Conteúdo – Uma combinação de testes visuais e de integração é a melhor escolha, pois os acordeões podem ser usados em combinação com muitos outros componentes
- Personalização – Podemos usar um teste de unidade para verificar se um nome de classe é aplicado corretamente, mas precisamos de um teste visual para garantir que o componente e os estilos personalizados funcionem em conjunto
- Callbacks – Os testes de unidade são ideais para garantir que os callbacks corretos sejam invocados
A Pirâmide de Teste do Acordeão
Testes de unidade
O conjunto completo de testes de unidade pode ser encontrado aqui. Cobrimos todas as alterações de estado, a personalização e os retornos de chamada:

it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })Testes de regressão visual
Os testes visuais estão localizados neste bloco de descrição do Cypress. As capturas de tela podem ser encontradas no painel do Happo.
Você pode ver que todos os diferentes estados de componentes, variantes e personalizações foram registrados. Toda vez que um PR é aberto, o CI compara as capturas de tela que o Happo armazenou com as capturadas em sua filial:
it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })Testes de integração
Escrevemos um teste de “caminho ruim” neste bloco de descrição do Cypress que afirma que o Accordion ainda funciona corretamente e que os usuários podem interagir com o componente personalizado. Também adicionamos afirmações visuais para maior confiança:
describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })Integração contínua
O Picasso depende quase inteiramente do GitHub Actions para controle de qualidade. Além disso, adicionamos ganchos do Git para verificações de qualidade de código de arquivos testados. Recentemente, migramos do Jenkins para o GHA, então nossa configuração ainda está no estágio de MVP.
O fluxo de trabalho é executado em cada mudança na filial remota em ordem sequencial, sendo a integração e os testes visuais a última etapa porque são mais caros de executar (tanto em relação ao desempenho quanto ao custo monetário). A menos que todos os testes sejam concluídos com êxito, a solicitação pull não poderá ser mesclada.
Estas são as etapas pelas quais o GitHub Actions passa sempre:
- Instalação de dependência
- Controle de versão – Valida se o formato dos commits e o título do PR correspondem aos commits convencionais
- Lint – ESlint garante código de boa qualidade
- Compilação TypeScript – Verifique se não há erros de tipo
- Compilação de pacotes – Se os pacotes não puderem ser compilados, eles não serão lançados com sucesso; nossos testes Cypress também esperam código compilado
- Testes de unidade
- Integração e testes visuais
O fluxo de trabalho completo pode ser encontrado aqui. Atualmente, leva menos de 12 minutos para concluir todas as etapas.
Testabilidade
Como a maioria das bibliotecas de componentes, o Picasso tem um componente raiz que deve envolver todos os outros componentes e pode ser usado para definir regras globais. Isso torna mais difícil escrever testes por dois motivos: inconsistências nos resultados dos testes, dependendo das props usadas no wrapper; e clichê extra:
import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })Resolvemos o primeiro problema criando um TestingPicasso que pré-condiciona as regras globais para teste. Mas é irritante ter que declará-lo para cada caso de teste. É por isso que criamos uma função de renderização personalizada que envolve o componente passado em um TestingPicasso e retorna tudo o que está disponível na função de renderização da RTL.
Nossos testes agora são mais fáceis de ler e simples de escrever:
import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })Conclusão
A configuração descrita aqui está longe de ser perfeita, mas é um bom ponto de partida para aqueles que são aventureiros o suficiente para criar uma biblioteca de componentes. Já li muito sobre testar pirâmides, mas nem sempre é fácil aplicá-las na prática. Portanto, convido você a explorar nossa base de código e aprender com nossos erros e acertos.
As bibliotecas de componentes são exclusivas porque atendem a dois tipos de público: os usuários finais que interagem com a interface do usuário e os desenvolvedores que usam seu código para criar seus próprios aplicativos. Investir tempo em uma estrutura de teste robusta beneficiará a todos. Investir tempo em melhorias de testabilidade beneficiará você como mantenedor e os engenheiros que usam (e testam) sua biblioteca.
Não discutimos coisas como cobertura de código, testes de ponta a ponta e políticas de versão e lançamento. O conselho curto sobre esses tópicos é: lance com frequência, pratique o versionamento semântico adequado, tenha transparência em seus processos e defina expectativas para os engenheiros que dependem de sua biblioteca. Podemos revisitar esses tópicos com mais detalhes em postagens subsequentes.
