Ideias para configurar, organizar e consolidar seus testes Cypress #frontend@twiliosendgrid

Publicados: 2020-12-15

Escrevemos muitos testes Cypress em diferentes aplicativos da Web e equipes de front-end aqui no Twilio SendGrid. À medida que nossos testes foram dimensionados para muitos recursos e páginas, nos deparamos com algumas opções úteis de configuração e desenvolvemos maneiras de manter melhor nosso crescente número de arquivos, seletores para os elementos de nossa página e valores de tempo limite ao longo do caminho.

Nosso objetivo é mostrar a você essas dicas e ideias para configurar, organizar e consolidar suas próprias coisas relacionadas ao Cypress, então sinta-se à vontade para fazer com elas o que funciona melhor para você e sua equipe.

Esta postagem do blog pressupõe que você tenha algum conhecimento prático dos testes Cypress e esteja procurando ideias para manter e melhorar seus testes Cypress. No entanto, se você estiver curioso para aprender mais sobre funções, asserções e padrões comuns que podem ser úteis para escrever testes Cypress em ambientes separados, você pode conferir esta postagem de blog com uma visão geral de mil pés.

Caso contrário, vamos continuar e primeiro orientá-lo em algumas dicas de configuração para o Cypress.

Configurando seu cypress.json

O arquivo cypress.json é onde você pode definir toda a configuração para seus testes Cypress , como tempos limite de base para seus comandos Cypress, variáveis ​​de ambiente e outras propriedades.

Aqui estão algumas dicas para você experimentar em sua configuração:

  • Ajuste os tempos limite do comando básico, para que você não precise sempre adicionar { timeout: timeoutInMs } em todos os seus comandos Cypress. Mexa com os números até encontrar o equilíbrio certo para configurações como “defaultCommandTimeout”, “requestTimeout” e “responseTimeout”.
  • Se seu teste envolver iframes, você provavelmente precisará definir “chromeWebSecurity” como false para poder acessar iframes de origem cruzada em seu aplicativo.
  • Tente bloquear scripts de marketing, análise e log de terceiros ao executar seus testes Cypress para aumentar a velocidade e não acionar eventos indesejados. Você pode facilmente configurar uma lista de negação com sua propriedade “blacklistHosts” (ou propriedade “blockHosts” no Cypress 5.0.0), levando em uma matriz de globs de string que correspondem aos caminhos de host de terceiros.
  • Ajuste as dimensões padrão da janela de visualização com “viewportWidth” e “viewportHeight” para quando você abrir a GUI do Cypress para facilitar a visualização.
  • Se houver problemas de memória no Docker para testes pesados ​​ou você quiser ajudar a tornar as coisas mais eficientes, tente modificar “numTestsKeptInMemory” para um número menor que o padrão e definir “videoUploadOnPasses” como false para se concentrar no upload de vídeos para teste com falha corre apenas.

Em outra nota, depois de ajustar sua configuração do Cypress, você também pode adicionar o TypeScript e digitar melhor seus testes do Cypress, como fizemos nesta postagem do blog. Isso é especialmente benéfico para preenchimento automático, avisos de tipo ou erros ao chamar e encadear funções (como cy.task('someTaskPluginFunction) , Cypress.env('someEnvVariable') ou cy.customCommand() ).

Você também pode experimentar configurar um arquivo cypress.json básico e carregar um arquivo de configuração Cypress separado para cada ambiente de teste, como um staging.json ao executar scripts Cypress diferentes em seu package.json . A documentação do Cypress faz um ótimo trabalho ao orientá-lo nessa abordagem.

Organizando sua pasta Cypress

O Cypress já configura uma pasta cypress de nível superior com uma estrutura guiada quando você inicia o Cypress em sua base de código. Isso inclui outras pastas, como integration para seus arquivos de especificações, fixtures para arquivos de dados JSON, plugins para suas cy.task(...) e outras configurações, e uma pasta de support para seus comandos e tipos personalizados.

Uma boa regra a seguir ao organizar dentro de suas pastas Cypress, componentes React ou qualquer código em geral é colocar coisas juntas que provavelmente mudarão juntas. Nesse caso, como estamos lidando com aplicativos da Web no navegador, uma abordagem que se adapta bem é agrupar as coisas em uma pasta por página ou recurso abrangente.

Criamos uma pasta de pages separada particionada pelo nome do recurso, como “SenderAuthentication” para manter todos os objetos de página abaixo da rota /settings/sender_auth , como “DomainAuthentication” (/settings/sender_auth/domains/**/*) e “LinkBranding” (/settings/sender_auth/links/**/*). Em nossa pasta de plugins , também fizemos a mesma coisa ao organizar todos os arquivos de plugin cy.task(...) para um determinado recurso ou página na mesma pasta para autenticação de remetente, e seguimos a mesma abordagem para nossa pasta de integration para os arquivos de especificação. Escalamos nossos testes para centenas de arquivos de especificação, objetos de página, plug-ins e outros arquivos em uma de nossas bases de código - e é fácil e conveniente navegar.

Aqui está um esboço de como organizamos a pasta cypress .

Uma outra coisa a considerar ao organizar sua pasta de integration (onde todas as suas especificações residem) é potencialmente dividir os arquivos de especificações com base na prioridade do teste. Por exemplo, você pode ter todos os testes de prioridade e valor mais altos em uma pasta “P1” e os testes de segunda prioridade em uma pasta “P2” para que você possa executar facilmente todos os testes “P1” definindo a opção --spec como --spec 'cypress/integration/P1/**/*' .

Crie uma hierarquia de pastas de especificações que funcione para você para que você possa agrupar especificações facilmente não apenas por página ou recurso como --spec 'cypress/integration/SomePage/**/*' , mas também por outros critérios como prioridade, produto , ou ambiente.

Consolidando seletores de elementos

Ao desenvolver os componentes React da nossa página, normalmente adicionamos algum nível de testes unitários e de integração com Jest e Enzyme. No final do desenvolvimento de recursos, adicionamos outra camada de testes Cypress E2E para garantir que tudo funcione com o back-end. Tanto os testes de unidade para nossos componentes React quanto os objetos de página para nossos testes Cypress E2E requerem seletores para os componentes/elementos DOM na página com a qual desejamos interagir e afirmar.

Quando atualizamos essas páginas e componentes, há uma chance de haver desvios e erros de ter que sincronizar vários locais de seletores de teste de unidade para o objeto de página Cypress para o próprio código do componente real. Se confiássemos apenas em nomes de classes relacionados a estilos, seria difícil lembrar de atualizar todos os lugares que podem quebrar. Em vez disso, adicionamos “data-hook”, “data-testid” ou qualquer outro atributo “data-*” consistentemente nomeado para componentes e elementos específicos na página que desejamos lembrar e escrevemos um seletor para ele em nossas camadas de teste.

Podemos anexar atributos “data-hook” a muitos de nossos elementos, mas ainda precisávamos de uma maneira de agrupá-los em um só lugar para atualizar e reutilizar em outros arquivos. Descobrimos uma maneira tipificada de gerenciar todos esses seletores de “gancho de dados” para serem distribuídos em nossos componentes React e para serem utilizados em nossos testes de unidade e seletores de objetos de página para mais reutilização e manutenção mais fácil em objetos exportados.

Para a pasta de nível superior de cada página, criaríamos um arquivo hooks.ts que gerencia e exporta um objeto com um nome de chave legível para o elemento e o seletor CSS de string real para o elemento como valor. Vamos chamá-los de “Read Selectors”, pois precisamos ler e usar o formulário de seletor CSS para um elemento em chamadas como wrapper.find(“[data-hook='selector']”) em nossos testes de unidade ou Cypress's cy.get(“[data-hook='selector']”) em nossos objetos de página. Essas chamadas ficariam mais limpas como wrapper.find(Selectors.someElement) ou cy.get(Selectors.someElement) .

O trecho de código a seguir aborda mais sobre por que e como estamos usando esses seletores de leitura na prática.

Da mesma forma, também exportamos objetos com um nome de chave legível para o elemento e com um objeto como { “data-hook”: “selector” } como valor. Vamos chamá-los de “Seletores de Gravação”, pois precisamos escrever ou espalhar esses objetos em um componente React como props para adicionar com sucesso o atributo “data-hook” aos elementos subjacentes. O objetivo seria fazer algo assim e o elemento DOM real abaixo - supondo que as props sejam passadas para os elementos JSX corretamente - também terá o atributo "data-hook=" definido.

O trecho de código a seguir aborda mais sobre por que e como estamos usando esses seletores de gravação na prática.

Podemos criar objetos para consolidar nossos seletores de leitura e seletores de gravação para atualizar menos lugares, mas e se tivéssemos que escrever muitos seletores para algumas de nossas páginas mais complexas? Isso pode ser mais propenso a erros para construir você mesmo, então vamos criar funções para gerar facilmente esses seletores de leitura e seletores de gravação para eventualmente exportar para uma determinada página.

Para a função geradora de seletor de leitura, percorreremos as propriedades de um objeto de entrada e formaremos a string seletora CSS [data-hook=”selector”] para cada nome de elemento. Se o valor correspondente de uma chave for null , assumiremos que o nome do elemento no objeto de entrada será o mesmo que o valor “data-hook”, como { someElement: null } => { someElement: '[data-hook=”someElement”] } . Caso contrário, se o valor correspondente de uma chave não for nulo, podemos optar por substituir esse valor “data-hook” como { someElement: “newSelector” } => { someElement: '[data-hook=”newSelector”]' } .

Para a função geradora de seletor de gravação, percorreremos as propriedades de um objeto de entrada e formaremos os objetos { “data-hook”: “selector” } para cada nome de elemento. Se o valor correspondente de uma chave for null , assumiremos que o nome do elemento no objeto de entrada será o mesmo que o valor “data-hook” como { someElement: null } => { someElement: { “data-hook”: “someElement” } } . Caso contrário, se o valor correspondente de uma chave não for null , podemos optar por substituir esse valor “data-hook” como { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }' } .

Usando essas duas funções geradoras, construímos nossos objetos seletores de leitura e seletores de gravação para uma página e os exportamos para serem reutilizados em nossos testes de unidade e objetos de página Cypress. Outro bônus é que esses objetos são digitados de tal forma que não podemos acidentalmente fazer Selectors.unknownElement ou WriteSelectors.unknownElement em nossos arquivos TypeScript também. Antes de exportar nossos seletores de leitura, também permitimos adicionar elementos extras e mapeamentos de seletores CSS para componentes de terceiros sobre os quais não temos controle. Em alguns casos, não podemos adicionar um atributo “data-hook” a determinados elementos, então ainda precisamos selecionar por outros atributos, ids e classes e adicionar mais propriedades ao objeto seletor de leitura conforme mostrado abaixo.

Esse padrão nos ajuda a nos manter organizados com todos os nossos seletores para uma página e para quando precisamos atualizar as coisas. Recomendamos investigar maneiras de gerenciar todos esses seletores em algum tipo de objeto ou por qualquer outro meio para minimizar quantos arquivos você precisa tocar em alterações futuras.

Consolidando tempos limite

Uma coisa que notamos depois de escrever muitos testes Cypress foi que nossos seletores para certos elementos atingiriam o tempo limite e levariam mais tempo do que os valores de tempo limite padrão definidos em nosso cypress.json , como “defaultCommandTimeout” e “responseTimeout”. Voltamos e ajustamos certas páginas que precisavam de tempos limite mais longos, mas, com o tempo, o número de valores de tempo limite arbitrários cresceu e mantê-lo tornou-se mais difícil para alterações em grande escala.

Como resultado, consolidamos nossos tempos limite em um objeto começando acima de nosso “defaultCommandTimeout”, que está em algum lugar no intervalo de cinco a dez segundos para cobrir a maioria dos tempos limite gerais para nossos seletores, como cy.get(...) ou cy.contains(...) . Além desse tempo limite padrão, escalaríamos para “curto”, “médio”, “longo”, “xlongo” e “xxlong” dentro de um objeto de tempo limite que podemos importar em qualquer lugar em nossos arquivos para usar em nossos comandos Cypress, como cy.get(“someElement”, { timeout: timeouts.short }) ou cy.task('pluginName', {}, { timeout: timeouts.xlong }) . Depois de substituir nossos tempos limite por esses valores de nosso objeto importado, temos um local para atualizar para aumentar ou diminuir o tempo que leva para determinados tempos limite.

Um exemplo de como você pode facilmente começar com isso é mostrado abaixo.

Empacotando

À medida que seus conjuntos de testes Cypress crescem, você pode se deparar com alguns dos mesmos problemas que tivemos ao descobrir a melhor forma de dimensionar e manter seus testes Cypress. Você pode optar por organizar seus arquivos de acordo com a página, recurso ou alguma outra convenção de agrupamento, para que sempre saiba onde procurar arquivos de teste existentes e onde adicionar novos à medida que mais desenvolvedores contribuem para sua base de código.

À medida que a interface do usuário muda, você pode usar algum tipo de objeto digitado (como os seletores de leitura e gravação) para manter seus seletores de atributo “data-” para os elementos-chave de cada página que você gostaria de afirmar ou interagir em seus testes de unidade e Cypress testes. Se você começar a aplicar muitos valores arbitrários para coisas como valores de tempo limite para seus comandos Cypress, talvez seja hora de configurar um objeto preenchido com valores dimensionados para que você possa atualizar esses valores em um só lugar.

À medida que as coisas mudam em sua interface de usuário de front-end, API de back-end e testes Cypress, você deve sempre pensar em maneiras de manter esses testes com mais facilidade. Menos lugares para atualizar e cometer erros e menos ambiguidade sobre onde colocar as coisas fazem uma enorme diferença, pois muitos desenvolvedores adicionam novas páginas, recursos e (inevitavelmente) testes Cypress no caminho.

Interessado em mais posts sobre Cypress? Dê uma olhada nos seguintes artigos:

  • O que considerar ao escrever testes E2E
  • Visão geral de 1.000 pés para escrever testes de cipreste
  • TypeScript Todas as coisas em seus testes Cypress
  • Lidando com fluxos de e-mail em testes Cypress
  • Integrando testes Cypress com Docker, Buildkite e CICD