Teste de regressão visual com Cypress: uma abordagem pragmática
Publicados: 2022-03-11Sempre que uma nova versão de nossa biblioteca de componentes, Picasso, é lançada, atualizamos todos os nossos aplicativos front-end para aproveitar ao máximo os novos recursos e alinhar nossos designs em todas as partes do nosso site.
No mês passado, lançamos uma atualização do Picasso para o Toptal Talent Portal, a plataforma que nossos talentos usam para encontrar empregos e interagir com clientes. Sabendo que o lançamento viria com grandes mudanças de design e, em um esforço para minimizar problemas inesperados, fazia sentido usar técnicas de teste de regressão visual para nos ajudar a encontrar problemas antes do lançamento.
O teste de regressão visual não é um conceito novo; muitos outros projetos da Toptal já o utilizam, incluindo o próprio Picasso.
Ferramentas como Percy, Happo e Chromatic podem ser usadas para ajudar as equipes a criar um pipeline de regressão visual saudável, e consideramos adicioná-las primeiro. Por fim, decidimos que o processo de configuração seria muito demorado e poderia atrapalhar nosso cronograma. Já tínhamos uma data marcada para um congelamento de código para iniciar a migração e, faltando apenas alguns dias para o prazo final, não tivemos escolha a não ser ser criativos.
Teste de regressão visual por meio de teste de interface do usuário
Embora não tivéssemos testes de regressão visual no projeto, tivemos uma boa cobertura de testes de integração de interface do usuário usando Cypress. Mesmo que não seja para isso que a ferramenta é mais usada, o Cypress tem uma página em sua documentação dedicada a testes visuais e outra que lista todos os plug-ins disponíveis para ajudar a configurar o Cypress para testes visuais.
Do Cypress às capturas de tela
Depois de passar pela documentação disponível, decidimos experimentar o cypress-snapshot-plugin. Levou apenas alguns minutos para configurar e, assim que o fizemos, percebemos rapidamente que não estávamos em busca de uma saída de regressão visual tradicional.
A maioria das ferramentas de regressão visual ajuda a identificar alterações indesejadas comparando instantâneos e detectando diferenças de pixel entre uma linha de base conhecida e aceita e a versão modificada de uma página ou componente. Se a diferença de pixel for maior que um limite de tolerância definido, a página ou o componente será sinalizado para ser examinado manualmente. Nesta versão, porém, sabíamos que teríamos várias pequenas alterações na maioria dos nossos componentes de interface do usuário, portanto, definir um limite não era aplicável. Mesmo que um determinado componente seja 100% diferente, ele ainda pode estar correto no contexto da nova versão. Da mesma forma, um desvio tão pequeno quanto alguns pixels pode significar que um componente não está adequado para produção no momento.
Nesse ponto, duas coisas contrastantes ficaram claras: observar as diferenças de pixel não ajudaria a identificar problemas, e ter uma comparação lado a lado dos componentes era exatamente o que precisávamos. Colocamos o plug-in de instantâneo de lado e começamos a criar uma coleção de imagens com nossos componentes antes e depois da aplicação da atualização do Picasso. Dessa forma, poderíamos examinar rapidamente todas as alterações para determinar se as novas versões ainda correspondiam às necessidades do site e aos padrões da biblioteca.
O novo plano era fazer uma captura de tela de um componente, armazená-lo localmente, fazer uma nova captura de tela do mesmo componente na ramificação com a versão atualizada do Picasso e, em seguida, mesclá-los em uma única imagem. Por fim, essa nova abordagem não foi muito diferente da que começamos, mas nos deu mais flexibilidade durante a fase de implementação, pois não precisávamos mais importar o plug-in e usar seus novos comandos.
Aproveitando APIs para imagens de comparação
Com um objetivo claro em mente, era hora de ver como o Cypress poderia nos ajudar a obter as capturas de tela que precisávamos. Como mencionado, tivemos uma boa quantidade de testes de interface do usuário cobrindo a maior parte do Portal de Talentos, então, em um esforço para coletar o maior número possível de componentes críticos, decidimos fazer capturas de tela de elementos individuais após cada interação.
Uma abordagem alternativa seria fazer capturas de tela da página inteira em momentos-chave durante o teste, mas decidimos que essas imagens seriam muito difíceis de comparar. Além disso, essas comparações podem ser mais propensas a erros humanos, como perder que um rodapé foi alterado.
Uma terceira opção seria passar por cada caso de teste para decidir o que capturar, mas isso levaria muito mais tempo, então manter todos os elementos usados nas páginas parecia um compromisso prático.
Nós recorremos à API do Cypress para gerar as imagens. O comando cy.screenshot() pode, pronto para uso, criar imagens individuais de componentes, e a API After Screenshot nos permite renomear arquivos, alterar diretórios e distinguir execuções de regressão visual das padrão. Ao combinar os dois, criamos execuções que não afetaram nossos testes funcionais e nos permitiram armazenar imagens em suas pastas apropriadas.
Primeiro, estendemos o arquivo index.js em nosso diretório de plug-ins para oferecer suporte aos dois novos tipos de execução (linha de base e comparação). Em seguida, definimos o caminho para nossas imagens de acordo com o tipo de execução:
// plugins/index.js const fs = require('fs') const path = require('path') module.exports = (on, config) => { // Adding these values to your config object allows you to access them in your tests. config.env.baseline = process.env.BASELINE || false config.env.comparison = process.env.COMPARISON || false on('after:screenshot', details => { // We only want to modify the behavior of baseline and comparison runs. if (config.env.baseline || config.env.comparison) { // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders. // An alternative would have been to look up the folder for the latest image, but this was the simpler approach. let lastScreenshotFile = '' let lastScreenshotNumber = 0 // We append the proper suffix number to the image, create the folder, and move the file. const createDirAndRename = filePath => { if (lastScreenshotFile === filePath) { lastScreenshotNumber++ } else { lastScreenshotNumber = 0 } lastScreenshotFile = filePath const newPath = filePath.replace( '.png', ` #${lastScreenshotNumber}.png` ) return new Promise((resolve, reject) => { fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => { if (mkdirErr) { return reject(mkdirErr) } fs.rename(details.path, newPath, renameErr => { if (renameErr) { return reject(renameErr) } resolve({ path: newPath }) }) }) }) } const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}` return createDirAndRename(details.path .replace('cypress/integration', screenshotPath) .replace('All Specs', screenshotPath) ) } }) return config } Em seguida, invocamos cada uma das execuções adicionando a variável de ambiente correspondente à chamada do Cypress no package.json do projeto:

"scripts": { "cypress:baseline": "BASELINE=true yarn cypress:open", "cypress:comparison": "COMPARISON=true yarn cypress:open" }Depois de executarmos nossos novos comandos, pudemos ver que todas as capturas de tela feitas durante a execução foram movidas para as pastas apropriadas.
Em seguida, tentamos sobrescrever cy.get() , o comando principal do Cypress para retornar elementos DOM e fazer uma captura de tela de qualquer elemento chamado junto com sua implementação padrão. Infelizmente, cy.get() é um comando complicado de mudar, pois chamar o comando original em sua própria definição leva a um loop infinito. A abordagem sugerida para contornar essa limitação é criar um comando personalizado separado e, em seguida, fazer com que esse novo comando faça uma captura de tela depois de encontrar o elemento:
Cypress.Commands.add("getAndScreenshot", (selector, options) => { // Note: You might need to tweak the command when getting multiple elements. return cy.get(selector).screenshot() }); it("get overwrite", () => { cy.visit("https://example.cypress.io/commands/actions"); cy.getAndScreenshot(".action-email") }) No entanto, nossas chamadas para interagir com os elementos na página já estavam encapsuladas em uma função getElement() interna. Então, tudo o que tínhamos a fazer era garantir que uma captura de tela fosse tirada quando o wrapper fosse chamado.
Resultados Obtidos Através do Teste de Regressão Visual
Uma vez que tínhamos as capturas de tela, a única coisa que restava a fazer era mesclá-las. Para isso, criamos um script de node simples usando o Canvas. No final, o script nos permitiu gerar 618 imagens de comparação! Algumas das diferenças foram fáceis de identificar abrindo o Portal de Talentos, mas alguns dos problemas não eram tão óbvios.
Agregando valor ao teste de interface do usuário
Em primeiro lugar, os testes de regressão visual adicionados provaram ser úteis e descobriram alguns problemas que poderíamos ter perdido sem eles. Mesmo que estivéssemos esperando diferenças em nossos componentes, saber o que realmente foi alterado ajudou a diminuir os casos problemáticos. Então, se o seu projeto tem uma interface, mas você ainda não está realizando esses testes, vá em frente!
A segunda lição aqui, e talvez a mais importante, é que fomos mais uma vez lembrados de que o perfeito é inimigo do bom. Se tivéssemos descartado a possibilidade de executar testes de regressão visual para esta versão porque não havia configuração anterior, poderíamos ter perdido alguns bugs durante a migração. Em vez disso, concordamos com um plano que, embora não fosse o ideal, era rápido de executar, trabalhamos em direção a ele e valeu a pena.
Para obter mais detalhes sobre a implementação de um pipeline de regressão visual robusto em seu projeto, consulte a página de testes visuais do Cypress, selecione a ferramenta que melhor atende às suas necessidades e assista aos vídeos tutoriais.
