Visão geral de 1.000 pés sobre como escrever testes de cipreste #frontend@twiliosendgrid

Publicados: 2020-10-03

No Twilio SendGrid, escrevemos centenas de testes Cypress end-to-end (E2E) e continuamos a escrever mais à medida que novos recursos são lançados em diferentes aplicativos e equipes da Web. Esses testes cobrem toda a pilha, verificando se os casos de uso mais comuns pelos quais um cliente passaria ainda funcionam depois de enviar novas alterações de código em nossos aplicativos.

Se você quiser primeiro dar um passo para trás e ler mais sobre como pensar sobre testes E2E em geral, fique à vontade para conferir esta postagem no blog e voltar a ela quando estiver pronto. Esta postagem no blog não exige que você seja um especialista em testes E2E, mas ajuda a ter o estado de espírito certo, pois você verá por que fizemos as coisas de determinada maneira em nossos testes. Se você estiver procurando por um tutorial mais passo a passo apresentando os testes do Cypress, recomendamos verificar os documentos do Cypress . Nesta postagem do blog, assumimos que você já viu ou escreveu muitos testes Cypress antes e está curioso para ver como outros escrevem testes Cypress para seus próprios aplicativos.

Depois de escrever muitos testes do Cypress, você começará a se notar usando funções, asserções e padrões semelhantes do Cypress para realizar o que precisa. Mostraremos as partes e estratégias mais comuns que usamos ou fizemos antes com o Cypress para escrever testes em ambientes separados, como dev ou staging. Esperamos que esta visão geral de 1.000 pés de como escrevemos os testes Cypress lhe dê ideias para comparar com os seus próprios e ajude a melhorar a maneira como você aborda os testes Cypress.

Contorno:

  1. Resumo da API Cypress
  2. Interagindo com os elementos
  3. Afirmando em elementos
  4. Lidando com APIs e serviços
  5. Fazendo requisições HTTP com cy.request(…)
  6. Criando plugins reutilizáveis ​​com cy.task()
  7. Zombando de solicitações de rede com cy.server() e cy.route()
  8. Comandos personalizados
  9. Sobre objetos de página
  10. Escolhendo não executar o código do lado do cliente com verificações window.Cypress
  11. Lidando com iframes
  12. Padronização em ambientes de teste

Resumo da API Cypress

Vamos começar analisando as partes que usamos com mais frequência com a API do Cypress.

Selecionando elementos

Há muitas maneiras de selecionar elementos DOM, mas você pode realizar a maior parte do que precisa fazer por meio desses comandos Cypress e geralmente pode encadear mais ações e declarações depois disso.

  • Obtendo elementos baseados em algum seletor CSS com cy.get(“[data-hook='someSelector']”) ou cy.find(“.selector”) .
  • Selecionando elementos com base em algum texto como cy.contains(“someText”) ou obtendo um elemento com um determinado seletor que contém algum texto como cy.contains(“.selector”, “someText”) .
  • Obtendo um elemento pai para olhar “dentro”, para que todas as suas consultas futuras tenham escopo para os filhos do pai, como cy.get(“.selector”).within(() => { cy.get(“.child”) }) .
  • Encontrar uma lista de elementos e examinar “cada” um para realizar mais consultas e afirmações como cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” }) .
  • Às vezes, os elementos podem estar fora da visualização da página, então você precisará rolar o elemento para a visualização primeiro, como cy.get(“.buttonFarBelow”).scrollIntoView() .
  • Às vezes, você precisará de um tempo limite maior do que o tempo limite do comando padrão, portanto, opcionalmente, você pode adicionar um { timeout: timeoutInMs } como cy.get(“.someElement”, { timeout: 10000 }) .

Interagindo com os elementos

Essas são as interações mais usadas encontradas em nossos testes Cypress. Ocasionalmente, você precisará lançar uma propriedade { force: true } nessas chamadas de função para ignorar algumas verificações com os elementos. Isso geralmente ocorre quando um elemento é coberto de alguma forma ou derivado de uma biblioteca externa sobre a qual você não tem muito controle em termos de como ele renderiza os elementos.

  • Precisamos clicar em muitas coisas, como botões em modais, tabelas e similares, então fazemos coisas como cy.get(“.button”).click() .
  • Os formulários estão por toda parte em nossos aplicativos da web para preencher os detalhes do usuário e outros campos de dados. Digitamos nessas entradas com cy.get(“input”).type(“somekeyboardtyping”) e podemos precisar limpar alguns valores padrão de entradas limpando-os primeiro como cy.get(“input”).clear().type(“somenewinput”) . Há também maneiras legais de digitar outras teclas como {enter} para a tecla Enter quando você faz cy.get(“input”).type(“text{enter}”) .
  • Podemos interagir com opções de seleção como cy.get(“select”).select(“value”) e caixas de seleção como cy.get(“.checkbox”).check() .

Afirmando em elementos

Estas são as afirmações típicas que você pode usar em seus testes Cypress para determinar se as coisas estão presentes na página com o conteúdo certo.

  • Para verificar se as coisas aparecem ou não na página, você pode alternar entre cy.get(“.selector”).should(“be.visible”) e cy.get(“.selector”).should(“not.be.visible”) .
  • Para determinar se os elementos DOM existem em algum lugar na marcação e se você não se importa necessariamente se os elementos são visíveis, você pode usar cy.get(“.element”).should(“exist”) ou cy.get(“.element”).should(“not.exist”) .
  • Para ver se um elemento contém ou não algum texto, você pode escolher entre cy.get(“button”).should(“contain”, “someText”) e cy.get(“button”).should(“not.contain”, “someText”) .
  • Para verificar se uma entrada ou botão está desabilitado ou habilitado, você pode declarar assim: cy.get(“button”).should(“be.disabled”) .
  • Para confirmar se algo está marcado, você pode testar como, cy.get(“.checkbox”).should(“be.checked”) .
  • Normalmente, você pode confiar em verificações de texto e visibilidade mais tangíveis, mas às vezes você precisa confiar em verificações de classe como cy.get(“element”).should(“have.class”, “class-name”) . Existem outras maneiras semelhantes de testar atributos com .should(“have.attr”, “attribute”) .
  • Muitas vezes é útil para você encadear asserções também como, cy.get(“div”).should(“be.visible”).and(“contain”, “text”) .

Lidando com APIs e serviços

Ao lidar com suas próprias APIs e serviços relacionados a e-mail, você pode usar cy.request(...) para fazer solicitações HTTP para seus endpoints de back-end com cabeçalhos de autenticação. Outra alternativa é criar plugins cy.task(...) que podem ser chamados de qualquer arquivo de especificação para cobrir outras funcionalidades que podem ser melhor tratadas em um servidor Node com outras bibliotecas, como conectar-se a uma caixa de entrada de e-mail e encontrar um correspondência de e-mail ou ter mais controle sobre as respostas e pesquisas de determinadas chamadas de API antes de retornar alguns valores para os testes usarem.

Fazendo requisições HTTP com cy.request(…)

Você pode usar cy.request() para fazer solicitações HTTP à sua API de back-end para configurar ou eliminar dados antes da execução dos casos de teste. Você geralmente passa o URL do endpoint, o método HTTP como “GET” ou “POST”, cabeçalhos e, às vezes, um corpo de solicitação para enviar à API de back-end. Você pode encadear isso com um .then((response) => { }) para obter acesso à resposta da rede por meio de propriedades como “status” e “body”. Um exemplo de como fazer uma chamada cy.request() é demonstrado aqui.

Às vezes, você pode não se importar se o cy.request(...) falhará com um código de status 4xx ou 5xx durante a limpeza antes da execução de um teste. Um cenário em que você pode optar por ignorar o código de status com falha é quando seu teste faz uma solicitação GET para verificar se um item ainda existe e já foi excluído. O item já pode ser limpo e a solicitação GET falhará com um código de status 404 não encontrado. Nesse caso, você definiria outra opção de failOnStatusCode: false para que seus testes Cypress não falhem antes mesmo de executar as etapas de teste.

Criando plugins reutilizáveis ​​com cy.task()

Quando queremos ter mais flexibilidade e controle sobre uma função reutilizável para conversar com outro serviço, como um provedor de caixa de entrada de e-mail por meio de um servidor Node (vamos abordar este exemplo em uma postagem de blog posterior), gostamos de fornecer nossa própria funcionalidade extra e respostas personalizadas para chamadas de API para nós encadearmos e aplicarmos em nossos testes Cypress. Ou gostamos de executar algum outro código em um servidor Node - geralmente criamos um plug-in cy.task() para ele. Criamos funções de plugins em arquivos de módulos e as importamos no plugins/index.ts onde definimos os plugins de tarefas com os argumentos que precisamos para executar as funções conforme mostrado abaixo.

Esses plugins podem ser chamados com um cy.task(“pluginName”, { ...args }) em qualquer lugar em seus arquivos de especificação e você pode esperar que a mesma funcionalidade aconteça. Considerando que, se você usou cy.request() , você tem menos capacidade de reutilização, a menos que você envolva essas chamadas em objetos de página ou arquivos auxiliares a serem importados em todos os lugares.

Uma outra advertência é que, como o código da tarefa do plug-in deve ser executado em um servidor Node, você não pode chamar os comandos Cypress usuais dentro dessas funções, como Cypress.env(“apiHost”) ou cy.getCookie('auth_token') . Você passa coisas como a string do token de autenticação ou o host da API de back-end para o objeto de argumento da sua função de plug-in, além de coisas necessárias para o corpo da solicitação, se ele precisar conversar com sua API de back-end.

Zombando de solicitações de rede com cy.server() e cy.route()

Para testes do Cypress que exigem dados difíceis de reproduzir (como variações de estados importantes da interface do usuário em uma página ou lidando com chamadas de API mais lentas), um recurso do Cypress a ser considerado é eliminar as solicitações de rede. Isso funciona bem com solicitações baseadas em XmlHttpRequest (XHR) se você estiver usando vanilla XMLHttpRequest, a biblioteca axios ou jQuery AJAX. Você então usaria cy.server() e cy.route() para escutar rotas para simular respostas para qualquer estado desejado. Aqui está um exemplo:

Outro caso de uso é usar cy.server() , cy.route() e cy.wait() juntos para ouvir e esperar que as solicitações de rede terminem antes de executar as próximas etapas. Normalmente, depois de carregar uma página ou fazer algum tipo de ação na página, uma dica visual intuitiva sinalizará que algo está completo ou pronto para que possamos afirmar e agir. Para os casos em que você não tem uma sugestão tão visível, você pode esperar explicitamente que uma chamada de API termine assim.

Uma grande pegadinha é que, se você estiver usando fetch para solicitações de rede, não poderá simular as solicitações de rede ou esperar que elas terminem da mesma maneira. Você precisará de uma solução alternativa para substituir o window.fetch normal por um polyfill XHR e realizar algumas etapas de configuração e limpeza antes que seus testes sejam executados conforme registrado nesses problemas . Há também uma propriedade experimentalFetchPolyfill a partir do Cypress 4.9.0 que pode funcionar para você, mas, no geral, ainda estamos procurando métodos melhores para lidar com stubbing de rede em busca e uso de XHR em nossos aplicativos sem problemas. A partir do Cypress 5.1.0, há uma nova e promissora função cy.route2() (consulte a documentação do Cypress ) para stubs de rede experimentais de solicitações XHR e fetch, então planejamos atualizar nossa versão Cypress e experimentá-la para ver se resolve nossos problemas.

Comandos personalizados

Semelhante a bibliotecas como WebdriverIO, você pode criar comandos personalizados globais que podem ser reutilizados e encadeados em seus arquivos de especificação, como um comando personalizado para lidar com logins por meio da API antes da execução dos casos de teste. Depois de desenvolvê-los em um arquivo como support/commands.ts , você pode acessar as funções como cy.customCommand() ou cy.login() . Escrever um comando personalizado para fazer login se parece com isso.

Sobre objetos de página

Um objeto de página é um wrapper em torno de seletores e funções para ajudá-lo a interagir com uma página. Você não precisa criar objetos de página para escrever seus testes, mas é bom considerar maneiras de encapsular as alterações na interface do usuário. Você deseja facilitar suas vidas em termos de agrupamento de coisas para evitar a atualização de seletores e interações em vários arquivos em vez de em um só lugar.

Você pode definir uma classe base “Page” com funcionalidade comum, como open() para classes de página herdadas para compartilhar e estender. As classes de página derivadas definem suas próprias funções getter para seletores e outras funções auxiliares enquanto reutilizam a funcionalidade das classes base por meio de chamadas como super.open() como mostrado aqui.

Escolhendo não executar o código do lado do cliente com verificações window.Cypress

Quando testamos fluxos com arquivos de download automático, como um CSV, os downloads geralmente interrompem nossos testes Cypress ao congelar a execução do teste. Como compromisso, queríamos principalmente testar se o usuário poderia alcançar o estado de sucesso adequado para um download e não realmente baixar o arquivo em nosso teste, adicionando uma verificação window.Cypress .

Durante as execuções de teste do Cypress, haverá uma propriedade window.Cypress adicionada ao navegador. No código do lado do cliente, você pode optar por verificar se não há nenhuma propriedade Cypress no objeto da janela e, em seguida, realizar o download normalmente. Mas, se estiver sendo executado em um teste Cypress, não faça o download do arquivo. Também aproveitamos a verificação da propriedade window.Cypress para nossos experimentos A/B em execução em nosso aplicativo da web. Não queríamos adicionar mais comportamento irregular e não determinístico de experimentos A/B que potencialmente mostrassem experiências diferentes para nossos usuários de teste. Por isso, primeiro verificamos se a propriedade não está presente antes de executar a lógica do experimento, conforme destacado abaixo.

Lidando com iframes

Lidar com iframes pode ser difícil com o Cypress, pois não há suporte a iframe embutido. Há um [problema] em execução ( https://github.com/cypress-io/cypress/issues/136 ) preenchido com soluções alternativas para lidar com iframes únicos e iframes aninhados, que podem ou não funcionar dependendo da sua versão atual do Cypress ou o iframe com o qual você pretende interagir. Para nosso caso de uso, precisávamos de uma maneira de lidar com iframes de cobrança do Zuora em nosso ambiente de teste para verificar os fluxos de atualização da API de email e da API de campanhas de marketing. Nossos testes envolvem o preenchimento de informações de faturamento de amostra antes de concluir uma atualização para uma nova oferta em nosso aplicativo.

Criamos um comando personalizado cy.iframe(iframeSelector) para encapsular o tratamento de iframes. Passar um seletor para o iframe verificará o conteúdo do corpo do iframe até que ele não esteja mais vazio e, em seguida, retornará o conteúdo do corpo para que ele seja encadeado com mais comandos Cypress, conforme mostrado abaixo:

Ao trabalhar com o TypeScript, você pode digitar seu comando personalizado iframe assim em seu arquivo index.d.ts :

Para realizar a parte de cobrança de nossos testes, usamos o comando personalizado iframe para obter o conteúdo do corpo do iframe do Zuora e, em seguida, selecionamos os elementos dentro do iframe e alteramos seus valores diretamente. Anteriormente, tivemos problemas com o uso de cy.find(...).type(...) e outras alternativas que não funcionavam, mas felizmente encontramos uma solução alternativa alterando os valores das entradas e seleções diretamente com o comando invoke, ou seja, cy.get(selector).invoke('val', 'some value') . Você também precisará ”chromeWebSecurity”: false em seu arquivo de configuração cypress.json para permitir que você ignore quaisquer erros de origem cruzada. Um snippet de amostra de seu uso com seletores de preenchimento é fornecido abaixo:

Padronização em ambientes de teste

Depois de escrever testes com Cypress usando as asserções, funções e abordagens mais comuns destacadas anteriormente, podemos executar os testes e fazer com que eles passem em um ambiente. Este é um ótimo primeiro passo, mas temos vários ambientes para implantar novo código e testar nossas alterações. Cada ambiente tem seu próprio conjunto de bancos de dados, servidores e usuários, mas nossos testes Cypress devem ser escritos apenas uma vez para funcionar com as mesmas etapas gerais.

Para executar testes do Cypress em vários ambientes de teste, como desenvolvimento, teste e preparação antes de finalmente implantarmos nossas alterações na produção, precisamos aproveitar a capacidade do Cypress de adicionar variáveis ​​de ambiente e alterar valores de configuração para oferecer suporte a esses casos de uso.

Para executar seus testes em vários ambientes de front-end :

Você precisará alterar o valor “baseUrl” conforme acessado por meio de Cypress.config(“baseUrl”) para corresponder a esses URLs, como https://staging.app.com ou https://testing.app.com . Isso altera a URL base para todas as suas cy.visit(...) para anexar seus caminhos. Existem várias maneiras de definir isso, como definir CYPRESS_BASE_URL=<frontend_url> antes de executar seu comando Cypress ou definir --config baseUrl=<frontend_url> .

Para executar seus testes em diferentes ambientes de back -end :

Você precisa saber o nome do host da API como https://staging.api.com ou https://testing.api.com para definir em uma variável de ambiente como “apiHost” e acessada por meio de chamadas como Cypress.env(“apiHost”) . Eles serão usados ​​para suas cy.request(...) para fazer solicitações HTTP para determinados caminhos como “<apiHost>/some/endpoint” ou transmitidos para suas chamadas de função cy.task(...) como outro argumento propriedade para saber qual back-end atingir. Essas chamadas autenticadas também precisariam saber o token de autenticação que você provavelmente está armazenando em localStorage ou um cookie por meio de cy.getCookie(“auth_token”) . Certifique-se de que esse token de autenticação seja passado como parte do cabeçalho "Authorization" ou por algum outro meio como parte de sua solicitação. Existem várias maneiras de definir essas variáveis ​​de ambiente, como diretamente no arquivo cypress.json ou nas opções de linha de comando --env , onde você pode referenciá-las na documentação do Cypress .

Para abordar o login de usuários diferentes ou usar metadados variados:

Agora que você sabe como lidar com vários URLs de front-end e hosts de API de back-end, como você lida com o login de diferentes usuários? Como você usa metadados variados com base no ambiente, como coisas relacionadas a domínios, chaves de API e outros recursos que provavelmente serão exclusivos nos ambientes de teste?

Vamos começar criando outra variável de ambiente chamada “testEnv” com possíveis valores de “testing” e “staging” para que você possa usar isso como uma forma de informar quais usuários e metadados do ambiente aplicar no teste. Usando a variável de ambiente “testEnv”, você pode abordar isso de duas maneiras.

Você pode criar arquivos separados “staging.json”, “testing.json” e outros arquivos JSON de ambiente na pasta de fixtures e importá-los para você usar com base no valor “testEnv”, como cy.fixture(`${testEnv}.json`).then(...) . No entanto, você não pode digitar bem os arquivos JSON e há muito mais espaço para erros de sintaxe e ao escrever todas as propriedades necessárias por teste. Os arquivos JSON também estão mais distantes do código de teste, portanto, você teria que gerenciar pelo menos dois arquivos ao editar os testes. Problemas de manutenção semelhantes ocorreriam se todos os dados de teste de ambiente fossem definidos em variáveis ​​de ambiente diretamente em seu cypress.json e houvesse muitos para gerenciar em uma infinidade de testes.

Uma opção alternativa é criar um objeto de fixação de teste dentro do arquivo de especificação com propriedades baseadas em teste ou preparação para carregar o usuário e os metadados desse teste para um determinado ambiente. Como esses são objetos, você também pode definir um tipo TypeScript genérico melhor em torno de objetos de fixação de teste para todos os seus arquivos de especificação para reutilização e para definir os tipos de metadados. Você chamaria Cypress.env(“testEnv”) para ver em qual ambiente de teste você está executando e usar esse valor para extrair o dispositivo de teste do ambiente correspondente do objeto de dispositivo de teste geral e usar esses valores em seu teste. A ideia geral do objeto de acessórios de teste está resumida no trecho de código abaixo.

A aplicação do valor de configuração "baseUrl" do Cypress, da variável de ambiente de back-end "apiHost" e da variável de ambiente "testEnv" juntos nos permite ter testes do Cypress que funcionam em vários ambientes sem adicionar várias condições ou fluxos lógicos separados, conforme demonstrado abaixo.

Vamos dar um passo atrás para ver como você pode até mesmo fazer seus próprios comandos Cypress para serem executados através do npm. Conceitos semelhantes podem ser aplicados a yarn, Makefile e outros scripts que você possa estar usando para seu aplicativo. Você pode definir variações de comandos “open” e “run” para alinhar com o Cypress “open” na GUI e “run” no modo headless em vários ambientes frontend e backend em seu package.json . Você também pode configurar vários arquivos JSON para a configuração de cada ambiente, mas para simplificar, você verá os comandos com as opções e valores embutidos.

Você notará nos scripts package.json que seu frontend “baseUrl” varia de “http://localhost:9001” para quando você inicia seu aplicativo localmente para a URL do aplicativo implantado, como “ https://staging.app. com ”. Você pode definir as variáveis ​​“apiHost” e “testEnv” de back-end para ajudar a fazer solicitações para um endpoint de back-end e carregar um objeto específico de dispositivo de teste. Você também pode criar comandos “cicd” especiais para quando precisar executar seus testes em um contêiner do Docker com a chave de gravação.

Algumas dicas

Quando se trata de selecionar elementos, interagir com elementos e declarar elementos na página, você pode ir muito longe escrevendo muitos testes Cypress com uma pequena lista de comandos Cypress, como cy.get() , cy.contains() , .click() , .type() , .should('be.visible') .

Também há maneiras de fazer solicitações HTTP para uma API de back-end usando cy.request() , executar código arbitrário em um servidor Node com cy.task() e enviar solicitações de rede usando cy.server() e cy.route() . Você pode até criar seu próprio comando personalizado como cy.login() para ajudá-lo a fazer login em um usuário por meio da API. Todas essas coisas ajudam a redefinir um usuário para o ponto de partida adequado antes da execução dos testes. Envolva esses seletores e funções em um arquivo e você criou objetos de página reutilizáveis ​​para usar em suas especificações.

Para ajudá-lo a escrever testes que passam em mais de um ambiente, aproveite as variáveis ​​de ambiente e os objetos que contêm metadados específicos do ambiente.

Isso ajudará você a executar diferentes conjuntos de usuários com recursos de dados separados em suas especificações do Cypress. Comandos Cypress npm separados como npm run cypress:open:staging em seu package.json carregarão os valores de variáveis ​​de ambiente apropriados e executarão os testes para o ambiente que você escolheu para executar.

Isso encerra nossa visão geral de mil pés sobre como escrever testes Cypress. Esperamos que isso tenha fornecido exemplos e padrões práticos para aplicar e melhorar em seus próprios testes Cypress.

Interessado em saber mais sobre os testes Cypress? Confira os seguintes recursos:

  • O que considerar ao escrever testes E2E
  • TypeScript Todas as coisas em seus testes Cypress
  • Lidando com fluxos de e-mail em testes Cypress
  • Ideias para configurar, organizar e consolidar seus testes Cypress
  • Integrando testes Cypress com Docker, Buildkite e CICD