Mergulho profundo do Mirage JS: usando o Mirage JS e o Cypress para testes de interface do usuário (parte 4)

Publicados: 2022-03-10
Resumo rápido ↬ Nesta parte final da série Mirage JS Deep Dive, colocaremos tudo o que aprendemos na série anterior para aprender como realizar testes de interface do usuário com o Mirage JS.

Uma das minhas citações favoritas sobre teste de software é da documentação do Flutter. Diz:

“Como você pode garantir que seu aplicativo continue funcionando à medida que você adiciona mais recursos ou altera a funcionalidade existente? Escrevendo testes.”

Nessa nota, esta última parte da série Mirage JS Deep Dive se concentrará no uso do Mirage para testar seu aplicativo de front-end JavaScript.

Nota : Este artigo pressupõe um ambiente Cypress. Cypress é uma estrutura de teste para testes de interface do usuário. Você pode, no entanto, transferir o conhecimento aqui para qualquer ambiente ou estrutura de teste de interface do usuário que você usar.

Leia as partes anteriores da série:

  • Parte 1: Entendendo os modelos e associações do Mirage JS
  • Parte 2: Noções básicas sobre fábricas, acessórios e serializadores
  • Parte 3: Entendendo o Tempo, Resposta e Transmissão

Primer de testes de interface do usuário

O teste de interface do usuário ou interface do usuário é uma forma de teste de aceitação feito para verificar os fluxos de usuário do seu aplicativo front-end. A ênfase desses tipos de testes de software está no usuário final, que é a pessoa real que estará interagindo com seu aplicativo da Web em uma variedade de dispositivos, desde desktops, laptops e dispositivos móveis. Esses usuários fariam interface ou interagem com seu aplicativo usando dispositivos de entrada, como teclado, mouse ou telas sensíveis ao toque. Os testes de interface do usuário, portanto, são escritos para imitar a interação do usuário com seu aplicativo o mais próximo possível.

Vamos pegar um site de comércio eletrônico, por exemplo. Um cenário de teste de IU típico seria:

  • O usuário pode visualizar a lista de produtos ao visitar a página inicial.

Outros cenários de teste de IU podem ser:

  • O usuário pode ver o nome de um produto na página de detalhes do produto.
  • O usuário pode clicar no botão “adicionar ao carrinho”.
  • O usuário pode fazer o checkout.

Você entendeu a ideia, certo?

Ao fazer testes de interface do usuário, você dependerá principalmente de seus estados de back-end, ou seja, ele retornou os produtos ou um erro? O papel que o Mirage desempenha nisso é disponibilizar esses estados de servidor para você ajustar conforme necessário. Portanto, em vez de fazer uma solicitação real ao servidor de produção em seus testes de interface do usuário, você faz a solicitação ao servidor simulado do Mirage.

Para a parte restante deste artigo, realizaremos testes de interface do usuário em uma interface do usuário de aplicativo da Web de comércio eletrônico fictício. Então vamos começar.

Mais depois do salto! Continue lendo abaixo ↓

Nosso primeiro teste de interface do usuário

Como afirmado anteriormente, este artigo pressupõe um ambiente Cypress. Cypress torna o teste de interface do usuário na web rápido e fácil. Você pode simular cliques e navegação e pode visitar rotas programaticamente em seu aplicativo. Veja os documentos para saber mais sobre o Cypress.

Portanto, supondo que Cypress e Mirage estejam disponíveis para nós, vamos começar definindo uma função de proxy para sua solicitação de API. Podemos fazer isso no arquivo support/index.js de nossa configuração do Cypress. Basta colar o seguinte código em:

 // cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })

Em seguida, no arquivo de inicialização do seu aplicativo ( main.js para Vue, index.js para React), usaremos o Mirage para fazer proxy das solicitações de API do seu aplicativo para a função handleFromCypress somente quando o Cypress estiver em execução. Aqui está o código para isso:

 import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }

Com essa configuração, sempre que o Cypress estiver em execução, seu aplicativo saberá usar o Mirage como o servidor simulado para todas as solicitações de API.

Vamos continuar escrevendo alguns testes de interface do usuário. Começaremos testando nossa página inicial para ver se ela tem 5 produtos exibidos. Para fazer isso no Cypress, precisamos criar um arquivo homepage.test.js na pasta de tests na raiz do diretório do seu projeto. Em seguida, diremos ao Cypress para fazer o seguinte:

  • Visite a página inicial ou seja / rota
  • Em seguida, afirme se possui elementos li com a classe do product e também verifica se são 5 em números.

Aqui está o código:

 // homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Você deve ter adivinhado que esse teste falharia porque não temos um servidor de produção retornando 5 produtos para nosso aplicativo front-end. Então, o que fazemos? Nós zombamos do servidor no Mirage! Se trouxermos o Mirage, ele pode interceptar todas as chamadas de rede em nossos testes. Vamos fazer isso abaixo e iniciar o servidor Mirage antes de cada teste na função beforeEach e também desligá-lo na função afterEach . As funções beforeEach e afterEach são fornecidas pelo Cypress e foram disponibilizadas para que você possa executar o código antes e depois de cada teste executado em seu conjunto de testes - daí o nome. Então vamos ver o código para isso:

 // homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })

Ok, estamos chegando a algum lugar; importamos o servidor do Mirage e o estamos iniciando e encerrando nas funções beforeEach e afterEach , respectivamente. Vamos zombar do nosso recurso de produto.

 // homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Observação : você sempre pode dar uma olhada nas partes anteriores desta série se não entender os bits do Mirage do trecho de código acima.

  • Parte 1: Entendendo os modelos e associações do Mirage JS
  • Parte 2: Noções básicas sobre fábricas, acessórios e serializadores
  • Parte 3: Entendendo o Tempo, Resposta e Transmissão

Ok, começamos a detalhar nossa instância de servidor criando o modelo de produto e também criando o manipulador de rota para a rota /api/products . No entanto, se executarmos nossos testes, ele falhará porque ainda não temos nenhum produto no banco de dados Mirage.

Vamos preencher o banco de dados Mirage com alguns produtos. Para fazer isso, poderíamos ter usado o método create() em nossa instância do servidor, mas criar 5 produtos manualmente parece bastante tedioso. Deve haver uma maneira melhor.

Ah sim, existe. Vamos utilizar as fábricas (como explicado na segunda parte desta série). Precisaremos criar nossa fábrica de produtos assim:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Então, finalmente, usaremos createList() para criar rapidamente os 5 produtos que nosso teste precisa passar.

Vamos fazer isso:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Então, quando executamos nosso teste, ele passa!

Observação : após cada teste, o servidor do Mirage é desligado e redefinido, portanto, nenhum desses estados vazará nos testes.

Evitando vários servidores Mirage

Se você tem acompanhado esta série, notará quando estávamos usando o Mirage em desenvolvimento para interceptar nossas solicitações de rede; tínhamos um arquivo server.js na raiz do nosso aplicativo onde configuramos o Mirage. No espírito do DRY (Don't Repeat Yourself), acho que seria bom utilizar essa instância do servidor em vez de ter duas instâncias separadas do Mirage para desenvolvimento e teste. Para fazer isso (caso você ainda não tenha um arquivo server.js ), basta criar um no diretório src do seu projeto.

Nota : Sua estrutura será diferente se você estiver usando uma estrutura JavaScript, mas a ideia geral é configurar o arquivo server.js na raiz src do seu projeto.

Então, com essa nova estrutura, vamos exportar uma função em server.js que é responsável por criar nossa instância do servidor Mirage. Vamos fazer isso:

 // src/server.js export function makeServer() { /* Mirage code goes here */}

Vamos concluir a implementação da função makeServer removendo o servidor Mirage JS que criamos em homepage.test.js e adicionando-o ao corpo da função makeServer :

 import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Agora tudo que você precisa fazer é importar makeServer em seu teste. Usar uma única instância do Mirage Server é mais limpo; dessa forma, você não precisa manter duas instâncias de servidor para ambientes de desenvolvimento e teste.

Após importar a função makeServer , nosso teste agora deve ficar assim:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Portanto, agora temos um servidor Mirage central que nos atende tanto no desenvolvimento quanto no teste. Você também pode usar a função makeServer para iniciar o Mirage em desenvolvimento (veja a primeira parte desta série).

Seu código Mirage não deve entrar em produção. Portanto, dependendo de sua configuração de compilação, você só precisaria iniciar o Mirage durante o modo de desenvolvimento.

Nota : Leia meu artigo sobre como configurar API Mocking com Mirage e Vue.js para ver como eu fiz isso no Vue para que você possa replicar em qualquer estrutura de front-end que você use.

Ambiente de teste

O Mirage possui dois ambientes: desenvolvimento (padrão) e teste . No modo de desenvolvimento, o servidor Mirage terá um tempo de resposta padrão de 400ms (que você pode personalizar. Consulte o terceiro artigo desta série para isso), registra todas as respostas do servidor no console e carrega as sementes de desenvolvimento.

No entanto, no ambiente de teste, temos:

  • 0 atrasos para manter nossos testes rápidos
  • O Mirage suprime todos os logs para não poluir seus logs de CI
  • O Mirage também ignorará a função seeds() para que seus dados de seed possam ser usados ​​apenas para desenvolvimento, mas não vazarão em seus testes. Isso ajuda a manter seus testes determinísticos.

Vamos atualizar nosso makeServer para que possamos ter o benefício do ambiente de teste. Para fazer isso, faremos com que ele aceite um objeto com a opção de ambiente (o padrão será desenvolvido e o substituiremos em nosso teste). Nosso server.js agora deve ficar assim:

 // src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Observe também que estamos passando a opção de ambiente para a instância do servidor Mirage usando a abreviação da propriedade ES6. Agora com isso em vigor, vamos atualizar nosso teste para substituir o valor do ambiente para testar. Nosso teste agora está assim:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Teste AAA

A Mirage incentiva um padrão de teste chamado abordagem de teste triplo A ou AAA. Isso significa Organizar , Agir e Asserir . Você já pode ver essa estrutura em nosso teste acima:

 it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })

Você pode precisar quebrar esse padrão, mas 9 vezes em 10 deve funcionar bem para seus testes.

Vamos Testar Erros

Até agora, testamos nossa página inicial para ver se ela tem 5 produtos, no entanto, e se o servidor estiver inativo ou algo der errado ao buscar os produtos? Não precisamos esperar que o servidor fique inativo para trabalhar em como nossa interface do usuário ficaria nesse caso. Podemos simplesmente simular esse cenário com o Mirage.

Vamos retornar um 500 (erro do servidor) quando o usuário estiver na página inicial. Como vimos em um artigo anterior, para personalizar as respostas do Mirage utilizamos a classe Response. Vamos importá-lo e escrever nosso teste.

 homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Que mundo de flexibilidade! Acabamos de substituir a resposta que o Mirage retornaria para testar como nossa interface do usuário seria exibida se falhasse na busca de produtos. Nosso arquivo homepage.test.js geral agora ficaria assim:

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Observe que a modificação que fizemos no manipulador /api/products só existe em nosso teste. Isso significa que funciona como definimos anteriormente quando você está no modo de desenvolvimento.

Então, quando executarmos nossos testes, ambos devem passar.

Nota : Acredito que vale a pena notar que os elementos que estamos consultando no Cypress devem existir em sua interface do usuário front-end. Cypress não cria elementos HTML para você.

Testando a página de detalhes do produto

Por fim, vamos testar a interface do usuário da página de detalhes do produto. Então é isso que estamos testando:

  • O usuário pode ver o nome do produto na página de detalhes do produto

Vamos lá. Primeiro, criamos um novo teste para testar esse fluxo de usuário.

Aqui está o teste:

 it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Seu homepage.test.js deve finalmente ficar assim.

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Quando você executa seus testes, todos os três devem passar.

Empacotando

Foi divertido mostrar a vocês o interior do Mirage JS nesta série. Espero que você esteja melhor equipado para começar a ter uma melhor experiência de desenvolvimento front-end usando o Mirage para simular seu servidor back-end. Também espero que você use o conhecimento deste artigo para escrever mais testes de aceitação/IU/de ponta a ponta para seus aplicativos front-end.

  • Parte 1: Entendendo os modelos e associações do Mirage JS
  • Parte 2: Noções básicas sobre fábricas, acessórios e serializadores
  • Parte 3: Entendendo o Tempo, Resposta e Transmissão
  • Parte 4: Usando Mirage JS e Cypress para testes de interface do usuário