Uma jornada de teste E2E Parte 2: WebdriverIO para Cypress
Publicados: 2019-11-21Nota: Este é um post de #frontend@twiliosendgrid. Para outras postagens de engenharia, acesse o blog técnico.
Em todos os nossos aplicativos front-end, tínhamos e ainda temos o seguinte objetivo: fornecer uma maneira de escrever testes de automação E2E (end to end) consistentes, depuráveis, sustentáveis e valiosos para nossos aplicativos frontend e integrar com CICD (integração contínua e implantação contínua).
Para chegar ao estado que temos hoje em que centenas de testes E2E são possivelmente acionados ou executados em um cronograma em todas as nossas equipes de front-end no SendGrid, tivemos que pesquisar e experimentar muitas soluções potenciais ao longo do caminho até atingirmos esse objetivo principal. meta.
Tentamos lançar nossa própria solução Ruby Selenium personalizada desenvolvida por engenheiros de teste dedicados chamados SiteTestUI aka STUI, na qual todas as equipes poderiam contribuir para um repositório e executar testes de automação entre navegadores. Infelizmente, ele sucumbiu a testes lentos e irregulares, alarmes falsos, falta de contexto entre repositórios e idiomas, muitas mãos em uma cesta, experiências dolorosas de depuração e mais tempo gasto em manutenção do que em fornecer valor.
Em seguida, experimentamos outra biblioteca promissora no WebdriverIO para escrever testes em JavaScript colocados com os repositórios de aplicativos de cada equipe. Embora isso tenha resolvido alguns problemas, fortaleceu mais a propriedade da equipe e nos permitiu integrar testes com o Buildkite, nosso provedor de CICD, ainda tínhamos testes esquisitos que eram difíceis de depurar e difíceis de escrever, além de lidar com todos os bugs e peculiaridades do Selenium muito semelhantes .
Queríamos evitar outro STUI 2.0 e começamos a explorar outras opções. Se você quiser ler mais sobre nossas lições aprendidas e estratégias descobertas ao longo do caminho ao migrar do STUI para o WebdriverIO, confira a parte 1 da série de postagens do blog.
Nesta parte final da série de postagens do blog, abordaremos nossa jornada de STUI e WebdriverIO para Cypress e como passamos por migrações semelhantes na configuração da infraestrutura geral, escrevendo testes E2E organizados, integrando com nossos pipelines Buildkite e dimensionando para outras equipes de front-end na organização.
TLDR: Adotamos Cypress em vez de STUI e WebdriverIO e cumprimos todos os nossos objetivos de escrever valiosos testes E2E para integração com CICD. Muito do nosso trabalho e das lições aprendidas com o WebdriverIO e o STUI foram transferidos muito bem para a forma como usamos e integramos os testes Cypress hoje.
Índice
Explorando e pousando em Cypress
Mudando de STUI e WebdriverIO para Cypress
Etapa 1: instalando dependências para Cypress
Etapa 2: configurações e scripts de ambiente
Primeira passagem de configurações e scripts de ambiente
Evolução de configurações e scripts de ambiente
Etapa 3: Implementando testes E2E localmente
Etapa 4: Dockerizando os testes
Etapa 5: Integração com o CICD
Etapa 6: Comparando Cypress vs. WebdriverIO/STUI
Etapa 7: dimensionar para outras equipes de front-end
O que esperamos com o Cypress
Adotando o Cypress no futuro
Explorando e pousando em Cypress
Quando procuramos alternativas ao WebdriverIO, vimos outros wrappers de Selenium, como Protractor e Nightwatch, com um recurso semelhante definido para WebdriverIO, mas achamos que provavelmente teríamos configurações longas, testes esquisitos e depuração e manutenção tediosas no futuro.
Felizmente, tropeçamos em uma nova estrutura de teste E2E chamada Cypress, que apresentou configurações rápidas, testes rápidos e depuráveis executados no navegador, stub de solicitação de camada de rede e, o mais importante, não usando Selenium sob o capô.
Ficamos maravilhados com recursos incríveis, como gravações de vídeo, a GUI do Cypress, o serviço de painel pago e a paralelização para experimentar. Estávamos dispostos a comprometer o suporte entre navegadores em favor de testes valiosos passando consistentemente no Chrome com um repertório de ferramentas à nossa disposição para implementar, depurar e manter os testes para nossos desenvolvedores e controle de qualidade.
Também apreciamos as estruturas de teste, bibliotecas de asserção e todas as outras ferramentas escolhidas para termos uma abordagem mais padronizada de testes em todas as nossas equipes de front-end. Abaixo, fornecemos uma captura de tela das diferenças entre uma solução como WebdriverIO e Cypress e, se você quiser ver mais diferenças entre as soluções Cypress e Selenium, consulte a documentação sobre como funciona.
Com isso em mente, nos comprometemos a testar o Cypress como outra solução para escrever testes E2E rápidos, consistentes e depuráveis para serem eventualmente integrados ao Buildkite durante o CICD. Também definimos deliberadamente outro objetivo de comparar o Cypress com as soluções anteriores baseadas em Selenium para determinar, em última análise, por pontos de dados, se devemos continuar procurando ou construir nossos conjuntos de testes com o Cypress daqui para frente. Planejamos converter os testes WebdriverIO existentes e quaisquer outros testes de alta prioridade ainda em STUI para testes Cypress e comparar nossas experiências de desenvolvedor, velocidade, estabilidade, tempos de execução de teste e manutenção dos testes.
Mudando de STUI e WebdriverIO para Cypress
Ao mudar de STUI e WebdriverIO para Cypress, abordamos isso sistematicamente por meio da mesma estratégia de alto nível que usamos quando tentamos nossa migração de STUI para WebdriverIO em nossos repositórios de aplicativos de front-end. Para obter mais detalhes sobre como realizamos essas etapas para o WebdriverIO, consulte a parte 1 da série de postagens do blog. As etapas gerais para fazer a transição para o Cypress envolveram o seguinte:
- Instalando e configurando dependências para conectar com Cypress
- Estabelecendo configurações de ambiente e comandos de scripts
- Implementando testes E2E que passam localmente em diferentes ambientes
- Dockerizando os testes
- Integrando testes Dockerizados com o Buildkite, nosso provedor de CICD
Para atingir nossos objetivos secundários, também adicionamos etapas extras para comparar o Cypress com as soluções anteriores do Selenium e, eventualmente, dimensionar o Cypress em todas as equipes de front-end da organização:
6. Comparando o Cypress em termos de experiência do desenvolvedor, velocidade e estabilidade dos testes vs. WebdriverIO e STUI
7. Escalando para outras equipes de front-end
Etapa 1: instalando dependências para Cypress
Para começar a funcionar rapidamente com o Cypress, tudo o que tivemos que fazer foi `npm install cypress` em nossos projetos e iniciar o Cypress pela primeira vez para que ele fosse automaticamente apresentado com um arquivo de configuração `cypress.json` e uma pasta cypress
com acessórios iniciais, testes e outros arquivos de configuração para comandos e plugins. Apreciamos como o Cypress veio junto com o Mocha como o executor de testes, Chai para asserções e Chai-jQuery e Sinon-Chai para ainda mais asserções para usar e encadear. Não precisávamos mais gastar muito tempo pesquisando sobre qual executor de testes, relator, asserções e bibliotecas de serviço instalar e usar em comparação com quando começamos com o WebdriverIO ou STUI. Imediatamente executamos alguns dos testes gerados com a interface gráfica do Cypress e exploramos os muitos recursos de depuração à nossa disposição, como depuração de viagem no tempo, playground seletor, vídeos gravados, capturas de tela, logs de comando, ferramentas de desenvolvedor de navegador etc.
Também o configuramos posteriormente com Eslint e TypeScript para verificação de tipo estático extra e regras de formatação a serem seguidas ao confirmar um novo código de teste Cypress. Inicialmente, tivemos alguns problemas com o suporte a TypeScript e alguns arquivos que precisavam ser arquivos JavaScript, como aqueles centrados nos arquivos de plugins, mas na maioria das vezes conseguimos digitar a maioria de nossos arquivos para nossos testes, objetos de página e comandos.
Aqui está um exemplo de estrutura de pastas que uma de nossas equipes de front-end seguiu para incorporar objetos de página, plugins e comandos:
Etapa 2: configurações e scripts de ambiente
Depois de instalar e configurar rapidamente o Cypress para ser executado localmente, precisávamos de uma maneira para que nossos testes do Cypress fossem executados com configurações variadas para cada ambiente e queríamos oferecer suporte aos mesmos casos de uso que nossos comandos do WebdriverIO nos permitiram fazer. Aqui está uma lista que ilustra a maioria dos casos de uso de como queríamos executar esses testes e por que desejamos apoiá-los:
- Contra um servidor de desenvolvimento Webpack rodando em localhost (ou seja, http://localhost:8000) e esse servidor de desenvolvimento seria apontado para uma certa API de ambiente (ou seja, https://testing.api.com ou https://staging.api. com) como teste ou encenação.
Por quê? Às vezes precisamos fazer alterações em nosso aplicativo da web local, como adicionar seletores mais específicos para nossos testes interagirem com os elementos de maneira mais robusta ou estávamos em andamento no desenvolvimento de um novo recurso e precisávamos ajustar e validar os testes de automação existentes passaria localmente contra nossas novas alterações de código. Sempre que o código do aplicativo mudou e ainda não fizemos push para o ambiente implantado, usamos esse comando para executar nossos testes em nosso aplicativo Web local. - Contra um aplicativo implantado para um determinado ambiente (ou seja, https://testing.app.com ou https://staging.app.com), como teste ou teste
Por quê? Outras vezes, o código do aplicativo não muda, mas podemos ter que alterar nosso código de teste para corrigir alguma falha ou nos sentimos confiantes o suficiente para adicionar ou excluir testes completamente sem fazer nenhuma alteração no frontend. Utilizamos muito esse comando para atualizar ou depurar testes localmente em relação ao aplicativo implantado para simular mais de perto como nossos testes são executados no CICD. - Executando em um contêiner do Docker em um aplicativo implantado para um determinado ambiente, como teste ou preparo
Por quê? Isso se destina ao CICD para que possamos acionar testes E2E para serem executados em um contêiner do Docker, por exemplo, no aplicativo implantado de teste e garantir que eles sejam aprovados antes de implantar o código na produção ou em execuções de teste agendadas em um pipeline dedicado. Ao configurar esses comandos inicialmente, realizamos muitas tentativas e erros para ativar contêineres do Docker com diferentes valores de variáveis de ambiente e testar para ver os testes adequados executados com sucesso antes de conectá-los ao nosso provedor CICD, Buildkite.
Primeira passagem de configurações e scripts de ambiente
Quando experimentamos pela primeira vez a configuração do Cypress, fizemos isso no repositório que abrange https://app.sendgrid.com, um aplicativo da web que inclui páginas de recursos como autenticação de remetente, atividade de e-mail e validação de e-mail, e inevitavelmente compartilhamos nossas descobertas e aprendizados com as equipes por trás do nosso aplicativo web de Campanhas de Marketing, que abrange o domínio https://mc.sendgrid.com. Desejamos executar nossos testes E2E em nosso ambiente de teste e utilizamos a interface de linha de comando do Cypress e opções como --config
ou --env
para realizar nossos casos de uso.
Para executar os testes Cypress no aplicativo da Web localmente em http://127.0.0.1:8000
ou no URL do aplicativo de teste implantado, ajustamos o sinalizador de configuração baseUrl
no comando e adicionamos variáveis de ambiente extras, como testEnv
para ajudar carregue certos equipamentos ou dados de teste específicos do ambiente em nossos testes Cypress. Por exemplo, as chaves de API usadas, os usuários criados e outros recursos podem ser diferentes entre os ambientes. Utilizamos testEnv
para alternar esses fixtures ou adicionar lógica condicional especial se alguns recursos não fossem suportados em um ambiente ou a configuração de teste fosse diferente e acessássemos o ambiente por meio de uma chamada como Cypress.env(“testEnv”)
em nossas especificações.
Em seguida, organizamos nossos comandos cypress:open:*
para representar a abertura da GUI do Cypress para selecionarmos nossos testes para serem executados na interface do usuário quando desenvolvemos localmente e cypress:run:*
para denotar a execução de testes no modo headless, que foi mais adaptado para execução em um contêiner do Docker durante o CICD. O que veio depois open
ou run
seria o ambiente para que nossos comandos fossem lidos facilmente como npm run cypress:open:localhost:staging
para abrir a GUI e executar testes em um servidor de desenvolvimento Webpack local apontando para APIs de teste ou npm run cypress:run:staging
para executar testes no modo headless no aplicativo de teste implantado e na API. Os scripts do package.json
Cypress ficaram assim:
Evolução de configurações e scripts de ambiente
Em outro projeto, evoluímos nossos comandos e configurações do Cypress para aproveitar alguma lógica Node no arquivo cypress/plugins/index.js
para ter um arquivo cypress.json
base e arquivos de configuração separados que seriam lidos com base em uma variável de ambiente chamada configFile
para carregar um arquivo de configuração específico. Os arquivos de configuração carregados seriam então mesclados com o arquivo base para eventualmente apontar para um servidor de teste ou de backend simulado.
Caso você esteja se perguntando mais sobre o servidor de backend simulado, desenvolvemos um servidor Express com endpoints de backend simplesmente retornando respostas variadas de dados JSON estáticos e códigos de status (ou seja, 200, 4XX, 5XX) dependendo dos parâmetros de consulta passados nas solicitações. Isso desbloqueou o front-end para continuar a desenvolver os fluxos de página com chamadas de rede reais para o servidor de back-end simulado com respostas emulando a aparência da API real quando estiver disponível no futuro. Também poderíamos simular níveis variados de respostas de sucesso e erro facilmente para nossos diferentes estados de interface do usuário que seriam difíceis de reproduzir em produção e, como estaríamos fazendo chamadas de rede determinísticas, nossos testes Cypress seriam menos esquisitos ao disparar a mesma rede solicitações e respostas sempre.
Tínhamos um arquivo cypress.json
básico que incluía propriedades compartilhadas para tempos limite gerais, ID do projeto para conectar-se ao Cypress Dashboard Service, sobre o qual falaremos mais tarde, e outras configurações, conforme mostrado abaixo:
Criamos uma pasta de config
na pasta cypress
para armazenar cada um de nossos arquivos de configuração, como localhostMock.json
, para executar nosso servidor de desenvolvimento Webpack local em um servidor de API simulado local ou staging.json
para executar no aplicativo de teste implantado e na API. Esses arquivos de configuração a serem diferenciados e mesclados com a configuração base ficaram assim:
Os arquivos de configuração do CICD tinham um arquivo JSON ainda mais simples, pois precisávamos definir as variáveis de ambiente dinamicamente para considerar a URL base do front-end do serviço do Docker variável e os hosts da API do servidor simulado que analisaremos mais tarde.
Em nosso arquivo cypress/plugins/index.js
, adicionamos lógica para ler uma variável de ambiente chamada configFile
definida de um comando Cypress que eventualmente leria o arquivo correspondente na pasta config
e o mesclaria com o cypress.json
base, como abaixo:
Para escrever comandos Cypress sensatos com variáveis de ambiente definidas para nossos casos de uso, aproveitamos um Makefile
semelhante ao seguinte:
Com esses comandos bem organizados em um Makefile, poderíamos fazer rapidamente coisas como make cypress_open_staging
ou make cypress_run_staging
em nossos scripts npm `package.json`.
Antes, costumávamos colocar alguns comandos em uma longa linha que seria difícil de editar sem erros. Felizmente, o Makefile ajudou a espalhar as coisas muito melhor com interpolação legível de variáveis de ambiente nos comandos Cypress em várias linhas. Poderíamos definir ou exportar rapidamente variáveis de ambiente como configFile
para qual arquivo de configuração de ambiente carregar, BASE_URL
para visitar nossas páginas, API_HOST
para diferentes ambientes de back-end ou SPECS
para determinar quais testes executar antes de iniciar qualquer um dos comandos Makefile.
Também usamos comandos Makefile para outros scripts npm longos e comandos do Docker, como construir nossos ativos do Webpack, instalar dependências ou executar comandos simultaneamente com outros. Finalmente, traduziríamos alguns comandos do Makefile para a seção de scripts package.json
, embora isso não fosse necessário se alguém quisesse usar apenas o Makefile, e ficaria assim:
Propositadamente, deixamos de fora muitos comandos Cypress CICD, pois eles não eram comandos que seriam usados no desenvolvimento do dia a dia e, como resultado, mantivemos o package.json
mais simplificado. Mais importante, pudemos ver imediatamente todos os comandos Cypress relacionados ao servidor simulado e ao servidor de desenvolvimento Webpack local versus os ambientes de teste e quais estão “abrindo” a GUI em vez de “executar” no modo headless.
Etapa 3: Implementando testes E2E localmente
Quando começamos a implementar testes E2E com Cypress, referenciamos nossos testes existentes do WebdriverIO e STUI para converter e adicionar testes mais recentes para outros recursos de alta prioridade, desde simples verificações de integridade até fluxos de caminhos felizes complicados. Traduzir os objetos de página existentes e os arquivos de teste do WebdriverIO ou STUI para objetos de página e especificações equivalentes no Cypress provou ser muito fácil. Na verdade, resultou em um código muito mais limpo do que antes, com espera menos explícita por elementos e melhor encadeamento de asserções e outros comandos do Cypress.
Por exemplo, as etapas gerais dos testes permaneceram as mesmas da perspectiva do usuário final, portanto, o trabalho de conversão envolveu o mapeamento da API WebdriverIO ou STUI para a API Cypress das seguintes maneiras:
- Muitos comandos essencialmente apareceram e funcionaram de maneira semelhante ao ponto em que estávamos quase substituindo
$
oubrowser
porcy
ouCypress
ievisitando uma página através de$(“.button”).click()
paracy.get(“.button”).click()
,browser.url()
paracy.visit()
ou$(“.input”).setValue()
paracy.get(“.input”).type()
- Usar
$
ou$$
geralmente se transforma emcy.get(...)
oucy.contains(...)
, ou seja,$$(“.multiple-elements-selector”)
ou$(“.single-element-selector”)
transformado emcy.get(“.any-element-selector”)
,cy.contains(“text”)
oucy.contains(“.any-selector”)
- Removendo chamadas estranhas
$(“.selector”).waitForVisible(timeoutInMs)
,$(“.selector”).waitUntil(...)
ou$(“.selector”).waitForExist()
em favor de deixar o Cypress por padrão lidar com as tentativas e recuperar os elementos repetidamente comcy.get('.selector')
ecy.contains(textInElement)
. Se precisássemos de um tempo limite maior do que o padrão,cy.get('.selector', { timeout: longTimeoutInMs })
e depois de recuperar o elemento, encadeariamos o próximo comando de ação para fazer algo com o elemento, ou seja,cy.get(“.selector”).click()
. - Comandos personalizados com navegador.
addCommand('customCommand, () => {})` turned into `Cypress.Commands.add('customCommand', () => {})
e fazendo `cy.customCommand()` - Fazer solicitações de rede para configuração ou desmontagem por meio da API usando uma biblioteca chamada
node-fetch
e agrupando-a embrowser.call(() => return fetch(...))
e/oubrowser.waitUntil(...)
levou a fazendo solicitações HTTP em um servidor Cypress Node atravéscy.request(endpoint)
ou um plugin personalizado que definimos e fizemos chamadas comocy.task(taskToHitAPIOrService)
. - Antes, quando precisávamos esperar que uma solicitação de rede importante terminasse sem nenhuma alteração notável na interface do usuário, às vezes precisávamos recorrer ao uso de
browser.pause(timeoutInMs)
, mas com Cypress melhoramos isso com a funcionalidade de stubbing de rede e pudemos ouvir e espere que a solicitação específica termine comcy.server()
,cy.route(“method”, “/endpoint/we/are/waiting/for).as(“endpoint”)`, and `cy.wait(“@endpoint”)
antes de iniciar a ação que acionaria a solicitação.
Depois de traduzir muita sintaxe e comandos do WebdriverIO para comandos Cypress, trouxemos o mesmo conceito de ter um objeto de página base para funcionalidade compartilhada comum e objetos de página estendidos para cada página necessária para os testes. Aqui está um exemplo de um objeto de página base com uma funcionalidade open()
comum a ser compartilhada em todas as páginas.
Um objeto de página estendida adicionaria getters para seletores de elemento, implementaria a funcionalidade open()
com sua rota de página e forneceria qualquer funcionalidade auxiliar, conforme mostrado abaixo.
Nossos objetos de página estendidos reais também utilizaram um mapa de objetos simples para manter todos os nossos seletores CSS de elementos em um lugar que poderíamos conectar em nossos componentes React como atributos de dados, referência em testes de unidade e usar como nossos seletores em objetos de página Cypress. Além disso, nossas classes de objeto de página às vezes variavam ao tirar proveito do construtor de classe se, digamos, um objeto de página fosse reutilizado para várias páginas de aparência e funcionamento semelhantes, como nossas páginas de supressões, e passássemos argumentos para alterar a rota ou propriedades específicas.
Como observação lateral, as equipes não precisavam usar objetos de página, mas apreciamos a consistência do padrão para manter a funcionalidade da página e as referências do seletor de elemento DOM junto com uma estrutura de objeto de classe padrão para compartilhar funcionalidades comuns em todas as páginas. Outras equipes preferiram criar muitos arquivos diferentes com poucas funções utilitárias e sem o uso de classes ES6, mas o importante foi fornecer uma maneira organizada e previsível de encapsular tudo e escrever testes para melhor eficiência e manutenção do desenvolvedor.
Aderimos à mesma estratégia geral de teste usada com nossos testes WebdriverIO mais antigos, tentando configurar o teste o máximo possível por meio da API. Nós especialmente queríamos evitar construir nosso estado de configuração por meio da interface do usuário para não introduzir falhas e perda de tempo para as partes que não pretendíamos testar. A maioria dos testes envolveu esta estratégia:
- Configurando ou desfazendo por meio da API – Se precisássemos testar a criação de uma entidade por meio da interface do usuário, deveríamos primeiro excluir a entidade por meio da API. Independentemente de como a execução de teste anterior terminou com sucesso ou falha, o teste precisava ser configurado ou desmontado corretamente por meio da API para garantir que o teste se comportasse de maneira consistente e começasse com as condições corretas.
- Fazendo login em um usuário de teste dedicado por meio da API – Criamos usuários de teste dedicados por página ou mesmo por teste de automação para que nossos testes fossem isolados e não atrapalhassem os recursos uns dos outros quando executados em paralelo. Fizemos a mesma solicitação de nossa página de login por meio da API e armazenamos o cookie antes do início do teste para que pudéssemos visitar a página autenticada diretamente e iniciar as etapas reais do teste.
- Automatizando as etapas da perspectiva do usuário final – Depois de fazer login no usuário por meio da API, visitamos a página diretamente e automatizamos as etapas que um usuário final faria para concluir um fluxo de recursos e verificar se o usuário vê e interage com as coisas certas pelo caminho.
Para redefinir o teste de volta ao seu estado original esperado, deveríamos fazer login em um usuário de teste dedicado por meio da API com um comando global cy.login
, definir um cookie para manter o usuário conectado, fazer as chamadas de API necessárias para retornar o user para o estado inicial desejado por meio de cy.request(“endpoint”)
ou cy.task(“pluginAction”)
e visite a página autenticada que procuramos testar diretamente. Em seguida, automatizaríamos as etapas para realizar um fluxo de recursos do usuário, conforme mostrado no layout de teste abaixo.
Lembre-se dos comandos personalizados sobre os quais falamos para fazer login, cy.login()
, e sair, cy.logout()
? Nós os implementamos facilmente no Cypress dessa maneira, para que todos os nossos testes fizessem login em um usuário por meio da API da mesma maneira.
Além disso, queríamos automatizar e verificar certos fluxos complexos envolvendo e-mail que antes não podíamos fazer bem com WebdriverIO ou STUI. Alguns exemplos incluem exportar a atividade de email para um CSV, passar pelo fluxo Enviar para colega de trabalho para autenticação do remetente ou exportar os resultados da validação de email para um CSV. O Cypress impede que alguém acesse vários superdomínios em um teste, portanto, navegar para um cliente de e-mail por meio de uma interface do usuário que não possuímos era esquisito e não era uma opção.
Em vez disso, desenvolvemos plugins Cypress por meio de seus cy.task(“pluginAction”)
para usar algumas bibliotecas no servidor Cypress Node para se conectar a um cliente/caixa de entrada IMAP de e-mail de teste, como SquirrelMail, para verificar e-mails correspondentes em uma caixa de entrada após solicitar uma ação na interface do usuário e por seguir os links de redirecionamento desses e-mails de volta para o domínio do nosso aplicativo da web para verificar se determinadas páginas de download apareceram e concluir efetivamente todo o fluxo de clientes. Implementamos plugins que esperariam que os e-mails chegassem à caixa de entrada do SquirrelMail com determinadas linhas de assunto, excluir e-mails, enviar e-mails, acionar eventos de e-mail, pesquisar serviços de back-end e fazer configurações e desmontagens muito mais úteis por meio da API para nossos testes usarem.
Para fornecer mais informações sobre o que realmente testamos com o Cypress, cobrimos uma infinidade de casos de alto valor, como estes:
- Verificações de integridade de todas as nossas páginas, também conhecidas como passeios pelo aplicativo - queríamos garantir que as páginas carregadas com algum conteúdo, às vezes, certos serviços de back-end ou a hospedagem de front-end estivessem inoperantes. Também recomendamos fazer esses testes primeiro para construir a memória muscular mental de construir objetos de página com seletores e funções auxiliares e para obter testes rápidos e funcionais executados em um ambiente.
- Operações CRUD em uma página – sempre redefiniríamos os testes de acordo com a API e, em seguida, testamos especificamente a criação, leitura, atualização ou exclusão na interface do usuário. Por exemplo, se testássemos a capacidade de criar uma autenticação de domínio por meio da interface do usuário, independentemente de como a última execução de teste terminou, precisávamos garantir que o domínio que criaríamos por meio da interface do usuário foi excluído pela API antes de prosseguir com as etapas de interface do usuário automatizadas para criar o domínio e evitar colisões. Se testamos a capacidade de excluir uma supressão por meio da interface do usuário, criamos a supressão primeiro por meio da API e depois prosseguimos com as etapas.
- Testando filtros de pesquisa em uma página – Testamos a configuração de vários filtros de pesquisa avançada com Atividade de e-mail e visitando a página com parâmetros de consulta para garantir que os filtros fossem preenchidos automaticamente. Também adicionamos dados por meio da API para validação de e-mail e, mais uma vez, iniciamos diferentes filtros de pesquisa e validamos se a tabela correspondia aos filtros de pesquisa nessa página.
- Diferentes acessos de usuários – No Twilio SendGrid, temos contas pai que podem ter colegas de equipe com escopos variados ou permissões de acesso ou subusuários abaixo deles que também têm graus variados de acesso e se comportam de maneira semelhante a uma conta pai. Colegas de equipe com acesso somente leitura versus acesso de administrador para determinadas páginas e subusuários veriam ou não certas coisas em uma página e isso facilitou automatizar o login nesses tipos de usuários e verificar o que eles veem ou não veem nos testes do Cypress.
- Diferentes pacotes de usuário – Nossos usuários também podem variar nos tipos de pacotes gratuitos a pagos, como Essentials, Pro e Premier, e esses pacotes também podem ver ou não certas coisas em uma página. Faríamos login em usuários com pacotes diferentes e verificaríamos rapidamente os recursos, cópias ou páginas que os usuários tiveram acesso nos testes do Cypress.
Etapa 4: Dockerizando os testes
Ao executar cada etapa do pipeline Buildkite em uma nova máquina AWS na nuvem, não poderíamos simplesmente chamar npm run cypress:run:staging
porque essas máquinas não possuem Node, navegadores, nosso código de aplicativo ou qualquer outra dependência para realmente executar o Cypress testes. Quando configuramos o WebdriverIO antes, precisávamos montar três serviços separados em um arquivo Docker Compose para que os serviços de código de aplicativo, Selenium e Chrome apropriados operassem juntos para que os testes fossem executados.

Com o Cypress, foi muito mais simples, pois exigimos apenas a imagem do Docker base do Cypress, cypress/base
, para configurar o ambiente em um Dockerfile
e apenas um serviço em um arquivo docker-compose.yml
com nosso código de aplicativo para executar o Cypress testes. Examinaremos uma maneira de fazer isso, pois existem outras imagens do Cypress Docker para usar e outras maneiras de configurar os testes do Cypress no Docker. Nós encorajamos você a olhar para a documentação do Cypress para alternativas
Para abrir um serviço com todos os nossos aplicativos e códigos de teste necessários para executar os testes Cypress, criamos um Dockerfile
chamado Dockerfile.cypress
e instalamos todos os node_modules
e copiamos o código para o diretório de trabalho da imagem em um ambiente Node. Isso seria usado pelo nosso serviço cypress
Docker Compose e conseguimos a configuração do Dockerfile
da seguinte maneira:
Com este Dockerfile.cypress
, podemos integrar o Cypress para executar especificações selecionadas em uma determinada API de ambiente e aplicativo implantado por meio de um serviço Docker Compose chamado cypress
. Tudo o que tivemos que fazer foi interpolar algumas variáveis de ambiente, como SPECS
e BASE_URL
, para executar testes Cypress selecionados em uma determinada URL base por meio do npm run cypress:run:cicd:staging
, que se parece com isso, ”cypress:run:cicd:staging”: “cypress run --record --key --config baseUrl=$BASE_URL --env testEnv=staging”
.
Essas variáveis de ambiente seriam definidas por meio dos arquivos de configuração/configuração do pipeline Buildkite ou exportadas dinamicamente ao acionar testes Cypress para serem executados em nosso pipeline de implantação. Um exemplo de arquivo docker-compose.cypress.yml
era semelhante a este:
Há também algumas outras coisas a serem observadas. Por exemplo, você pode ver a variável de ambiente VERSION
que nos permite fazer referência a uma imagem específica do Docker marcada. Demonstraremos mais tarde como marcamos uma imagem do Docker e, em seguida, puxamos para baixo a mesma imagem do Docker para que essa compilação seja executada no código correto para os testes do Cypress.
Além disso, você também notará o BUILDKITE_BUILD_ID
passado, que vem de graça junto com outras variáveis de ambiente Buildkite para cada compilação que iniciamos, e o sinalizador ci-build-id
. Isso habilita o recurso de paralelização do Cypress e, quando definimos um certo número de máquinas alocadas para os testes do Cypress, ele automaticamente saberá como girar essas máquinas e separar nossos testes para executar em todos esses nós de máquina para otimizar e acelerar nosso teste tempos de execução.
Finalmente, também aproveitamos a montagem de volume e o recurso de artefatos do Buildkite. Carregamos os vídeos e capturas de tela para serem acessados diretamente por meio da guia "Artefatos" da interface do usuário do Buildkite, caso fiquemos sem nossas gravações de teste atribuídas pagas para o mês ou, de alguma forma, não possamos acessar o Serviço de painel. Sempre que alguém executa o comando Cypress “run” no modo headless, há saída nas pastas cypress/videos
e cypress/screenshots
para uma revisão local e simplesmente montamos essas pastas e as enviamos para o Buildkite para nós como uma proteção contra falhas.
Etapa 5: Integração com o CICD
Assim que conseguimos que os testes do Cypress fossem executados com sucesso em um contêiner do Docker em diferentes ambientes, começamos a integração com o Buildkite, nosso provedor de CICD. A Buildkite forneceu maneiras de executar etapas em um arquivo .yml
em nossas máquinas AWS com scripts Bash e variáveis de ambiente definidas no código ou por meio das configurações de pipeline Buildkite do repositório na interface do usuário da web. O Buildkite também nos permitiu acionar esse pipeline de teste de nosso pipeline de implantação principal com variáveis de ambiente exportadas e reutilizaríamos essas etapas de teste para outros pipelines de teste isolados que seriam executados em um cronograma para nosso controle de qualidade monitorar e analisar.
Em alto nível, nossos pipelines Buildkite de teste para Cypress e também nossos pipelines WebdriverIO anteriores compartilharam as seguintes etapas semelhantes:
- Configure as imagens do Docker . Crie, marque e envie as imagens do Docker necessárias para os testes para o registro para que possamos descarregá-las em uma etapa posterior.
- Execute os testes com base nas configurações de variáveis de ambiente . Puxe para baixo as imagens do Docker marcadas para a compilação específica e execute os comandos adequados em um ambiente implantado para executar conjuntos de testes selecionados das variáveis de ambiente definidas.
Aqui está um exemplo de um arquivo pipeline.cypress.yml
que demonstra como configurar as imagens do Docker na etapa “Build Cypress Docker Image” e executar os testes na etapa “Run Cypress tests”:
Uma coisa a notar é a primeira etapa, “Build Cypress Docker Image”, e como ele configura a imagem do Docker para o teste. Ele usou o comando de build
do Docker Compose para compilar o serviço cypress
com todo o código de teste do aplicativo e o marcou com a variável de ambiente latest
e ${VERSION}
para que possamos eventualmente extrair essa mesma imagem com a tag apropriada para esta compilação em um passo futuro. Cada etapa pode ser executada em uma máquina diferente na nuvem AWS em algum lugar, portanto, as tags identificam exclusivamente a imagem para a execução específica do Buildkite. Depois de marcar a imagem, enviamos a imagem mais recente e com a versão marcada para nosso registro privado do Docker para ser reutilizada.
Na etapa "Executar testes Cypress", puxamos para baixo a imagem que construímos, marcamos e enviamos na primeira etapa e iniciamos o serviço Cypress para executar os testes. Com base em variáveis de ambiente como SPECS
e BASE_URL
, executaríamos arquivos de teste específicos em um determinado ambiente de aplicativo implantado para essa compilação específica do Buildkite. Essas variáveis de ambiente seriam definidas por meio das configurações de pipeline do Buildkite ou seriam acionadas dinamicamente a partir de um script Bash que analisaria um campo de seleção do Buildkite para determinar quais suítes de teste executar e em qual ambiente.
Quando selecionamos quais testes serão executados durante nosso pipeline de implantação do Buildkite CICD e acionamos um pipeline de testes acionados dedicado com determinadas variáveis de ambiente exportadas, seguimos as etapas no arquivo pipeline.cypress.yml
para que isso aconteça. Um exemplo de acionamento dos testes após a implantação de algum novo código em um ambiente de ramificação de recurso do pipeline de implantação é semelhante a este:
Os testes acionados seriam executados em um pipeline separado e, após seguir o link “Build #639”, ele nos levaria às etapas de compilação para a execução do teste acionado, como abaixo:
Reutilizando o mesmo arquivo pipeline.cypress.yml
para nossos pipelines Cypress Buildkite dedicados em execução em um cronograma, temos compilações como a que executa nossos testes E2E “P1”, de maior prioridade, conforme mostrado na foto abaixo:
Tudo o que temos a fazer é definir as variáveis de ambiente adequadas para coisas como quais especificações executar e qual ambiente de back-end atingir nas configurações do pipeline do Buildkite. Em seguida, podemos configurar uma compilação agendada do Cron, que também está nas configurações do pipeline, para iniciar a cada determinado número de horas e pronto. We would then create many other separate pipelines for specific feature pages as needed to run on a schedule in a similar way and we would only vary the Cron schedule and environment variables while once again uploading the same `pipeline.cypress.yml` file to execute.
In each of those “Run Cypress tests” steps, we can see the console output with a link to the recorded test run in the paid Dashboard Service, the central place to manage your team's test recordings, billing, and other Cypress stats. Following the Dashboard Service link would take us to a results view for developers and QAs to take a look at the console output, screenshots, video recordings, and other metadata if required such as this:
Step 6: Comparing Cypress vs. WebdriverIO/STUI
After diving into our own custom Ruby Selenium solution in STUI, WebdriverIO, and finally Cypress tests, we recorded our tradeoffs between Cypress and Selenium wrapper solutions.
Prós
- It's not another Selenium wrapper – Our previous solutions came with a lot of Selenium quirks, bugs, and crashes to work around and resolve, whereas Cypress arrived without the same baggage and troubles to deal with in allowing us full access to the browser.
- More resilient selectors – We no longer had to explicitly wait for everything like in WebdriverIO with all the
$(.selector).waitForVisible()
calls and now rely oncy.get(...)
and cy.contains(...)
commands with their default timeout. It will automatically keep on retrying to retrieve the DOM elements and if the test demanded a longer timeout, it is also configurable per command. With less worrying about the waiting logic, our tests became way more readable and easier to chain. - Vastly improved developer experience – Cypress provides a large toolkit with better and more extensive documentation for assertions, commands, and setup. We loved the options of using the Cypress GUI, running in headless mode, executing in the command-line, and chaining more intuitive Cypress commands.
- Significantly better developer efficiency and debugging – When running the Cypress GUI, one has access to all of the browser console to see some helpful output, time travel debug and pause at certain commands in the command log to see before and after screenshots, inspect the DOM with the selector playground, and discern right away at which command the test failed. In WebdriverIO or STUI we struggled with observing the tests run over and over in a browser and then the console errors would not point us toward and would sometimes even lead us astray from where the test really failed in the code. When we opted to run the Cypress tests in headless mode, we got console errors, screenshots, and video recordings. With WebdriverIO we only had some screenshots and confusing console errors. These benefits resulted in us cranking out E2E tests much faster and with less overall time spent wondering why things went wrong. We recorded it took less developers and often around 2 to 3 times less days to write the same level of complicated tests with Cypress than with WebdriverIO or STUI.
- Network stubbing and mocking – With WebdriverIO or STUI, there was no such thing as network stubbing or mocking in comparison to Cypress. Now we can have endpoints return certain values or we can wait for certain endpoints to finish through
cy.server()
andcy.route()
. - Less time to set up locally – With WebdriverIO or STUI, there was a lot of time spent up front researching which reporters, test runners, assertions, and services to use, but with Cypress, it came bundled with everything and started working after just doing an
npm install cypress.
- Less time to set up with Docker – There are a bunch of ways to set up WebdriverIO with Selenium, browser, and application images that took us considerably more time and frustration to figure out in comparison to Cypress's Docker images to use right out of the gate.
- Parallelization with various CICD providers – We were able to configure our Buildkite pipelines to spin up a certain number of AWS machines to run our Cypress tests in parallel to dramatically speed up the overall test run time and uncover any flakiness in tests using the same resources. The Dashboard Service would also recommend to us the optimal number of machines to spin up in parallel for the best test run times.
- Paid Dashboard Service – When we run our Cypress tests in a Docker container in a Buildkite pipeline during CICD, our tests are recorded and stored for us to look at within the past month through a paid Dashboard Service. We have a parent organization for billing and separate projects for each frontend application to check out console output, screenshots, and recordings of all of our test runs.
- Tests are way more consistent and maintainable – Tests passed way more consistently with Cypress in comparison to WebdriverIO and STUI where the tests kept on failing so much to the point where they were often ignored. Cypress tests failing more often signaled actual issues and bugs to look into or suggested better ways to refactor our tests to be less flaky. With WebdriverIO and STUI, we wasted a lot more time in maintaining those tests to be somewhat useful, whereas with Cypress, we would every now and then adjust the tests in response to changes in the backend services or minor changes in the UI.
- Tests are faster – Builds passed way more consistently and overall test run times would be around 2 to 3 times faster when run serially without parallelization. We used to have overall test runs that would take hours with STUI and around 40 minutes with WebdriverIO, but now with way more tests and with the help of parallelization across many machine nodes, we can run over 200 tests in under 5 minutes .
- Room to grow with added features in the future – With a steady open-source presence and dedicated Cypress team working towards releasing way more features and improvements to the Cypress infrastructure, we viewed Cypress as a safer bet to invest in rather than STUI, which would require us to engineer and solve a lot of the headaches ourselves, and WebdriverIO, which appeared to feel more stagnant in new features added but with the same baggage as other Selenium wrappers.
Contras
- Lack of cross-browser support – As of this writing, we can only run our tests against Chrome. With WebdriverIO, we could run tests against Chrome, Firefox, Safari, and Opera. STUI also provided some cross-browser testing, though in a much limited form since we created a custom in-house solution
- Cannot integrate with some third-party services – With WebdriverIO, we had the option to integrate with services like BrowserStack and Sauce Labs for cross-browser and device testing. However, with Cypress there are no such third-party integrations but there are some plugins with services like Applitools for visual regression testing available. STUI, on the other hand, also had some small integrations with TestRail , but as a compromise, we log out the TestRail links in our Cypress tests so we can refer back to them if we needed to.
- Requires workarounds to test with iframes – There are some issues around handling iframes with Cypress. We ended up creating a global Cypress command to wrap how to deal with retrieving an iframe's contents as there is no specific API to deal with iframes like how WebdriverIO does.
To summarize our STUI, WebdriverIO, and Cypress comparison, we analyzed the overall developer experience (related to tools, writing tests, debugging, API, documentation, etc.), test run times, test passing rates, and maintenance as displayed in this table:
Following our analysis of the pros and cons of Cypress versus our previous solutions, it was pretty clear Cypress would be our best bet to accomplish our goal of writing fast, valuable, maintainable, and debuggable E2E tests we could integrate with CICD.
Though Cypress lacked features such as cross-browser testing and other integrations with third-party services that we could have had with STUI or WebdriverIO, we most importantly need tests that work more often than not and with the right tools to confidently fix broken ones. If we ever needed cross-browser testing or other integrations we could always still circle back and use our knowledge from our trials and experiences with WebdriverIO and STUI to still run a subset of tests with those frameworks.
We finally presented our findings to the rest of the frontend organization, engineering management, architects, and product. Upon demoing the Cypress test tools and showcasing our results between WebdriverIO/STUI and Cypress, we eventually received approval to standardize and adopt Cypress as our E2E testing library of choice for our frontend teams.
Step 7: Scaling to Other Frontend Teams
After successfully proving that using Cypress was the way to go for our use cases, we then focused on scaling it across all of our frontend teams' repos. We shared lessons learned and patterns of how to get up and running, how to write consistent, maintainable Cypress tests, and of how to hook those tests up during CICD or in scheduled Cypress Buildkite pipelines.
To promote greater visibility of test runs and gain access to a private monthly history of recordings, we established our own organization to be under one billing method to pay for the Dashboard Service with a certain recorded test run limit and maximum number of users in the organization to suit our needs.
Once we set up an umbrella organization, we invited developers and QAs from different frontend teams and each team would install Cypress, open up the Cypress GUI, and inspect the “Runs” and “Settings” tab to get the “Project ID” to place in their `cypress.json` configuration and “Record Key” to provide in their command options to start recording tests to the Dashboard Service. Finally, upon successfully setting up the project and recording tests to the Dashboard Service for the first time, logging into the Dashboard Service would show that team's repo under the “Projects” tab like this:
When we clicked a project like “mako”, we then had access to all of the test runs for that team's repo with quick access to console output, screenshots, and video recordings per test run upon clicking each row as shown below:
For more insights into our integration, we set up many separate dedicated test pipelines to run specific, crucial page tests on a schedule like say every couple hours to once per day. We also added functionality in our main Buildkite CICD deploy pipeline to select and trigger some tests against our feature branch environment and staging environment.
Como era de se esperar, isso rapidamente acabou com nossas execuções de teste gravadas alocadas para o mês, especialmente porque há várias equipes contribuindo e acionando testes de várias maneiras. Por experiência, recomendamos estar atento a quantos testes estão sendo executados em um cronograma, com que frequência esses testes são executados e quais testes são executados durante o CICD. Pode haver algumas execuções de teste redundantes e outras áreas mais econômicas, como diminuir a frequência de execuções de teste agendadas ou possivelmente se livrar de algumas completamente para acionar testes apenas durante o CICD.
A mesma regra de ser frugal se aplica à adição de usuários, pois enfatizamos fornecer acesso apenas a desenvolvedores e QAs nas equipes de front-end que usarão fortemente o Dashboard Service, em vez da alta gerência e outras pessoas fora dessas equipes para preencher essas vagas limitadas.
O que esperamos com o Cypress
Como mencionamos anteriormente, a Cypress demonstrou muita promessa e potencial de crescimento na comunidade de código aberto e com sua equipe dedicada encarregada de fornecer recursos mais úteis para usarmos em nossos testes E2E. Muitas das desvantagens que destacamos estão atualmente sendo abordadas e esperamos coisas como:
- Suporte entre navegadores – Este é um grande problema, porque muito do retrocesso da adoção do Cypress veio do uso apenas do Chrome em comparação com as soluções baseadas em Selenium, que suportavam navegadores como Firefox, Chrome e Safari. Felizmente, testes mais confiáveis, de fácil manutenção e depuração venceram nossa organização e esperamos aumentar nossos conjuntos de testes com mais testes entre navegadores no futuro, quando a equipe do Cypress lançar esse suporte entre navegadores.
- Reescrita da camada de rede – Isso também é enorme, pois tendemos a usar a API Fetch fortemente em nossas áreas React mais recentes e em áreas de aplicativos Backbone/Marionette mais antigas, ainda usamos jQuery AJAX e chamadas normais baseadas em XHR. Podemos facilmente stub out ou ouvir solicitações nas áreas XHR, mas tivemos que fazer algumas soluções hacky com fetch polyfills para obter o mesmo efeito. A reescrita da camada de rede deve ajudar a aliviar essas dores.
- Melhorias incrementais no Dashboard Service – Já vimos algumas novas alterações na interface do usuário do Dashboard Service recentemente e esperamos continuar a vê-lo crescer com mais visualizações de estatísticas e detalhamentos de dados úteis. Também usamos muito o recurso de paralelização e verificamos nossas gravações de teste com falha no Dashboard Service com frequência, portanto, seria bom ver quaisquer melhorias iterativas no layout e/ou recursos.
Adotando o Cypress no futuro
Para nossa organização, valorizamos a eficiência do desenvolvedor, a depuração e a estabilidade dos testes Cypress quando executados no navegador Chrome. Conseguimos testes consistentes e valiosos com menos manutenção no caminho e com muitas ferramentas para desenvolver novos testes e corrigir os existentes.
No geral, a documentação, API e ferramentas à nossa disposição superaram em muito os contras. Depois de experimentar o Cypress GUI e o Dashboard Service pago, definitivamente não queríamos voltar ao WebdriverIO ou à nossa solução Ruby Selenium personalizada.
Conectamos nossos testes com o Buildkite e alcançamos nosso objetivo de fornecer uma maneira de escrever testes de automação E2E consistentes, depuráveis, de fácil manutenção e valiosos para que nossos aplicativos front-end se integrem ao CICD . Provamos com evidências para o restante das equipes de front-end, superiores de engenharia e proprietários de produtos dos benefícios da adoção do Cypress e da eliminação do WebdriverIO e do STUI.
Centenas de testes posteriores em equipes de front-end no Twilio SendGrid e detectamos muitos bugs em nosso ambiente de teste e conseguimos corrigir rapidamente quaisquer testes esquisitos do nosso lado com muito mais confiança do que nunca. Desenvolvedores e QAs não temem mais a ideia de escrever testes E2E, mas agora esperam escrevê-los para cada novo recurso que lançarmos ou para cada recurso mais antigo que possa usar um pouco mais de cobertura.