One place for hosting & domains

      Testar

      Como testar um módulo do Node.js com o Mocha e o assert


      O autor selecionou a Open Internet/Free Speech Fund para receber uma doação como parte do programa Write for DOnations.

      Introdução

      Realizar testes é uma parte fundamental no desenvolvimento de software. É comum que os programadores executem códigos que testam seu aplicativo, enquanto eles fazem alterações para confirmar se o aplicativo está se comportando como eles gostariam. Com a configuração de teste correta, este processo pode até ser automatizado, economizando bastante tempo. Realizar testes consistentemente após escrever um novo código garante que novas alterações não quebrem recursos pré-existentes. Isso dá ao desenvolvedor confiança em sua base de códigos, especialmente quando ela é implantada para a produção, para que os usuários possam interagir com ela.

      Um framework de teste estrutura a maneira como criamos casos de teste. O Mocha é um framework de teste popular em JavaScript. Ele organiza nossos casos de teste e executa-os para nós. No entanto, o Mocha não verifica o comportamento do nosso código. Para comparar valores em um teste, podemos utilizar o módulo assert do Node.js.

      Neste artigo, você escreverá testes para um módulo de lista TODO (de AFAZERES) do Node.js. Você configurará e usará o framework de teste Mocha para estruturar seus testes. Então, usará o módulo assert do Node.js para criar os testes de fato. Neste sentido, você usará o Mocha como um construtor de planos e o assert para implementar o plano.

      Pré-requisitos

      • Node.js instalado em sua máquina de desenvolvimento. Este tutorial utiliza a versão 10.16.0 do Node.js. Para instalar essa versão em macOS ou Ubuntu 18.04, siga os passos descritos no artigo sobre Como instalar o Node.js e criar um ambiente de desenvolvimento local em macOS ou a seção entitulada Instalando usando um PPA, do artigo sobre Como instalar o Node.js no Ubuntu 18.04.
      • Um conhecimento básico do JavaScript, que pode ser encontrado em nossa série Como programar em JavaScript.

      Passo 1 — Escrevendo um módulo do Node

      Vamos começar este artigo escrevendo o módulo do Node.js que queremos testar. Este módulo gerenciará itens de uma lista de AFAZERES. Usando este módulo, seremos capazes de listar todos os AFAZERES dos quais estamos mantendo o controle, adicionar novos itens e marcar alguns como completos. Além disso, vamos conseguir exportar uma lista de AFAZERES para um arquivo CSV. Caso queira um lembrete sobre módulos do Node.js, leia nosso artigo sobre Como criar um módulo do Node.js.

      Primeiro, precisamos configurar o ambiente de programação. Crie uma pasta com o nome do seu projeto no terminal. Este tutorial usará o nome todos:

      Então, acesse aquela pasta:

      Agora, inicialize o npm, pois vamos usar sua funcionalidade CLI para executar os testes mais tarde:

      Temos apenas uma dependência, o Mocha, que usaremos para organizar e executar nossos testes. Para baixar e instalar o Mocha, use o seguinte:

      • npm i request --save-dev mocha

      Instalamos o Mocha como uma dependência dev, pois o módulo não exige a presença dela em uma configuração de produção. Caso queira aprender mais sobre os pacotes do Node.js ou o npm, confira nosso guia sobre Como usar os módulos do Node.js com o npm e o package.json.

      Por fim, vamos criar o arquivo que terá o código do nosso módulo:

      Com isso, estamos prontos para criar nosso módulo. Abra o index.js em um editor de texto como o nano:

      Vamos começar definindo a classeTodos. Essa classe contém todas as funções que precisamos para gerenciar nossa lista de AFAZERES. Adicione as linhas de código a seguir ao index.js:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      }
      
      module.exports = Todos;
      

      Começamos o arquivo criando uma classe Todos. Sua função constructor() não recebe argumentos, de forma que não precisamos fornecer valores para instanciar objetos para esta classe. Tudo o que fazemos ao inicializar um objeto de Todos é criar uma propriedade de todos que é uma matriz vazia.

      A linha modules (módulos) permite que outros módulos do Node.js exijam nossa classe Todos. Sem exportar explicitamente a classe, o arquivo de teste que criaremos mais tarde não seria capaz de usá-la.

      Vamos adicionar uma função para retornar a matriz de todos que armazenamos. Escreva nas seguintes linhas em destaque:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Nossa função list() retorna uma cópia da matriz que é usada pela classe. Ele faz uma cópia da matriz usando a sintaxe de desestruturação do JavaScript. Fazemos uma cópia da matriz, para que alterações que o usuário faz na matriz retornada por list() não afete a matriz usada pelo objeto Todos.

      Nota: as matrizes do JavaScript são tipos de referência. Isso significa que, para qualquer atribuição de variável a uma matriz ou invocação de função com uma matriz como parâmetro, o JavaScript se refere à matriz original criada. Por exemplo, caso tenhamos uma matriz com três itens chamados x e criemos uma nova variável, y, tal que y = x, y e x se referem à mesma coisa. Qualquer alteração que fizermos em y na matriz tem impacto na variável x e vice-versa.

      Agora, vamos escrever a função add(), que adiciona um novo item de AFAZERES:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      }
      
      module.exports = Todos;
      

      Nossa função add() recebe uma string e a coloca em uma nova propriedade de um objeto do JavaScript. O novo objeto também tem uma propriedade completed (finalizado), que é definida como false (falso) por padrão. Depois, adicionamos este novo objeto à nossa matriz de AFAZERES.

      Uma funcionalidade importante em um gestor de AFAZERES é marcar itens como finalizados. Para essa implantação, vamos fazer um loop que percorre nossa matriz todos para encontrar o item de AFAZERES que o usuário está procurando. Caso um seja encontrado, marcaremos o processo como concluído. Caso nenhum seja encontrado, emitiremos um erro.

      Adicione a função complete(), desta forma:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      
          complete(title) {
              let todoFound = false;
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      }
      
      module.exports = Todos;
      

      Salve o arquivo e saia do editor de texto.

      Temos agora um gerenciador de AFAZERES básico, que podemos testar. Em seguida, vamos testar manualmente nosso código para ver se o aplicativo está funcionando.

      Passo 2 — Testando o código manualmente

      Neste passo, executaremos as funções do nosso código e observaremos o resultado para garantir que ele corresponda às nossas expectativas. Isso é chamado de teste manual. É provável que seja aplicada a metodologia de testes mais comum. Apesar do fato de que iremos automatizar nosso teste mais tarde com o Mocha, vamos primeiro testar manualmente nosso código para ter uma melhor ideia de como o teste varia entre diferentes frameworks de teste.

      Vamos adicionar dois itens de AFAZERES ao nosso aplicativo e marcar um deles como completo. Inicie o REPL do Node.js na mesma pasta que o arquivo index.js:

      Você verá o prompt > no REPL que nos diz que podemos inserir o código do JavaScript. Digite o seguinte no prompt:

      • const Todos = require('./index');

      Com o require(), carregamos o módulo de AFAZERES em uma variável Todos. Lembre-se de que nosso módulo retorna a classe Todos por padrão.

      Agora, vamos instanciar um objeto para essa classe. No REPL, adicione esta linha de código:

      • const todos = new Todos();

      Podemos utilizar o objeto todos para verificar nossos trabalhos de implementação. Vamos adicionar nosso primeiro item de AFAZERES:

      Até agora, não vimos nenhum resultado em nosso terminal. Vamos verificar se armazenamos nosso item de AFAZERES "run code" (executar o código), obtendo uma lista de todos os nossos AFAZERES:

      Você verá este resultado em seu REPL:

      Output

      [ { title: 'run code', completed: false } ]

      Este é o resultado esperado: temos um item de AFAZERES em nossa matriz de AFAZERES. Além disso, o arquivo não está concluído por padrão.

      Vamos adicionar outro item de AFAZERES:

      • todos.add("test everything");

      Marque o primeiro item de AFAZERES como concluído:

      • todos.complete("run code");

      Nosso objeto todos agora está gerenciando dois itens: "run code" e "test everything" (testar tudo). O item de AFAZERES "run code" também será concluído. Vamos confirmar isso, chamando list() novamente:

      O REPL irá gerar como resultado:

      Output

      [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

      Agora, saia do REPL com o seguinte:

      Confirmamos que nosso módulo se comporta da maneira como esperávamos. Embora não tenhamos colocado nosso código em um arquivo de teste ou usado uma biblioteca de teste, testamos nosso código manualmente. Infelizmente, essa forma de teste gasta muito tempo se feita todas as vezes que fizermos uma mudança. Em seguida, vamos utilizar testes automatizados no Node.js e ver se podemos resolver esse problema com o framework de testes Mocha.

      No último passo, testamos manualmente nosso aplicativo. Isso funciona para casos individuais de uso. Entretanto, conforme nosso módulo aumenta em escala, menos viável se torna esse método. À medida que testamos novas características, precisamos ter certeza de que adicionamos funcionalidades que não criaram problemas com funcionalidades já existentes. Seria interessante testarmos todas as funcionalidades para cada alteração no código mais uma vez. Porém, fazer isso manualmente necessitaria de muito esforço e estaria propenso a erros.

      Uma prática mais eficiente poderia ser configurar testes automatizados. Esses testes seguem scripts escritos como qualquer outro bloco de código. Executamos nossas funções com entradas definidas e inspecionamos seus efeitos para garantir que se comportam como esperamos. À medida que nossa base de código cresce, também crescem nossos testes automatizados. Ao escrevermos novos testes juntamente com as funcionalidades, podemos verificar se todos os módulos ainda funcionam — tudo isso sem precisar lembrar como usar cada função toda vez.

      Neste tutorial, estamos usando o framework de testes Mocha com o módulo assert do Node.js. Vamos colocar um pouco a mão na massa para ver como eles funcionam juntos.

      Para começar, crie um novo arquivo para armazenar nosso código de teste:

      Agora, utilize seu editor de texto preferido para abrir o arquivo de teste. Você pode usar o nano, assim como anteriormente:

      Na primeira linha do arquivo de texto, carregaremos o módulo de AFAZERES, assim como fizemos no shell do Node.js. Depois disso, carregaremos o módulo assert para quando escrevermos nossos testes. Adicione as linhas a seguir:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      

      A propriedade strict do módulo assert nos permitirá usar testes de igualdade especiais recomendados pelo Node.js. Esses testes são também adequados para testes futuros, já que representam mais casos de uso.

      Antes de escrevermos os testes, vamos discutir como o Mocha organiza o nosso código. Geralmente, os testes estruturados no Mocha seguem este modelo:

      describe([String with Test Group Name], function() {
          it([String with Test Name], function() {
              [Test Code]
          });
      });
      

      Note duas funções chave: describe() e it(). A função describe() é usada para agrupar testes semelhantes. Ela não é exigida para que o Mocha realize testes, mas o agrupamento de testes torna nosso código de teste mais fácil de ser mantido. Recomenda-se que você agrupe seus testes de uma maneira que torne fácil a atualização dos que são semelhantes de uma só vez.

      O it() contém nosso código de teste. É aqui que interagimos com as funções do nosso módulo e usamos a biblioteca assert. Muitas funções it() podem ser definidas em uma função describe().

      Nosso objetivo nesta seção é usar o Mocha e o assert para automatizar nosso teste manual. Faremos isso passo a passo, começando com nosso bloco describe. Adicione isto ao seu arquivo após as linhas do módulo:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      Com este bloco de código, criamos um agrupamento para nossos testes integrados. Os testes de unidade testariam uma função de cada vez. Os integration tests (testes de integração) verificam o quão bem as funções dentro de um módulo de diferentes módulos funcionam juntas. Quando o Mocha executa nosso teste, todos os testes dentro desse bloco describe serão executados no grupo "integration test".

      Vamos adicionar uma função it(), para que possamos começar a testar o código do nosso módulo:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
          });
      });
      

      Note como escolhemos um nome descritivo para o teste. Caso alguém execute nosso teste, ficará imediatamente claro o que está passando ou falhando. Normalmente, um aplicativo bem testado é um aplicativo bem documentado. Além disso, os testes podem ser, por vezes, um tipo de documentação eficaz.

      Para nosso primeiro teste, criaremos um novo objeto Todos e verificaremos se ele não tem itens nele:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      A primeira linha de código instanciou um novo objeto Todos, assim como faríamos no REPL do Node.js ou outro módulo. Na segunda linha nova, usamos o módulo assert.

      A partir do módulo assert, usamos o método notStrictEqual(). Essa função recebe dois parâmetros: o valor que queremos testar (chamado de valor actual (real)) e o valor que esperamos obter (chamada de valor expected (esperado)). Caso ambos os argumentos sejam iguais, notStrictEqual() emite um erro para fazer o teste falhar.

      Salve e saia do index.test.js.

      O caso base será verdadeiro, pois o comprimento deveria ser 0, que é diferente de 1. Vamos confirmar isso executando o Mocha. Para fazer isso, precisamos modificar nosso arquivo package.json. Abra o seu arquivo package.json com seu editor de texto:

      Agora, modifique sua propriedade scripts para que se pareça com isto:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Acabamos de alterar o comportamento do comando CLI test do npm. Ao executarmos npm test, o npm irá revisar o comando que acabamos de digitar no package.json. Ela irá procurar pela biblioteca do Mocha em nossa pasta node_modules e executará o comando mocha com nosso arquivo de teste.

      Salve e saia do package.json.

      Vamos ver o que acontece ao executarmos nosso teste. Em seu terminal, digite:

      O comando gerará o seguinte resultado:

      Output

      > todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Em primeiro lugar, este resultado nos mostra qual grupo de testes está prestes a ser executado. Para cada teste dentro de um grupo, pula-se uma linha no caso de teste. Vemos nosso nome de teste da forma como o descrevemos na função it(). A marcação no lado esquerdo do caso de teste indica que o teste foi aprovado.

      No final, recebemos um resumo de todos os nossos testes. Em nosso caso, nosso único teste foi aprovado e foi concluído em 16ms (o tempo varia de computador para computador).

      Nossa testagem foi iniciada com sucesso. No entanto, este caso de teste atual pode permitir falsos positivos. Um falso positivo é um caso de teste que é aprovado quando na verdade deveria falhar.

      Neste momento, verificamos que o comprimento da matriz não é igual a 1. Vamos modificar o teste para que essa condição seja verdadeira quando não deveria. Adicione as linhas a seguir ao index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Salve e saia do arquivo.

      Adicionamos dois itens de AFAZERES. Vamos executar o teste para ver o que acontece:

      Isso resultará no seguinte:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

      O teste é aprovado conforme esperado, pois o comprimento é maior que 1. No entanto, ele anula o propósito original de ter aquele primeiro teste. O primeiro teste visa confirmar que estamos começando de um estado vazio. Um teste mais bem acabado confirmará isso, em todos os casos.

      Vamos alterar o teste, para que seja aprovado apenas se tivermos absolutamente nenhum item de AFAZERES em estoque. Faça as seguintes alterações no index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Você alterou o notStrictEqual() para strictEqual(), uma função que verifica se há uma igualdade entre seu argumento real e esperado. A função strict equal falhará no caso de nossos argumentos não serem exatamente iguais.

      Salve, saia e então execute o teste para que possamos ver o que acontece:

      Desta vez, o resultado mostrará um erro:

      Output

      ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

      Este texto será útil para que possamos depurar o porquê do teste ter falhado. Note que, como o teste falhou, não houve marcador no início do caso de teste.

      Nosso resumo de teste já não está mais no final do resultado, mas sim logo após a exibição de nossa lista de casos de teste:

      ...
      0 passing (29ms)
        1 failing
      ...
      

      O resultado que restou nos fornece dados sobre nossos testes que falharam. Primeiro, vemos qual caso de teste falhou:

      ...
      1) integrated test
             should be able to add and complete TODOs:
      ...
      

      Em seguida, vemos o motivo pelo qual nosso teste falhou:

      ...
            AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
      + expected - actual
      
      - 2
      + 0
            + expected - actual
      
            -2
            +0
      
            at Context.<anonymous> (index.test.js:9:10)
      ...
      

      Um AssertionError é lançado quando o strictEqual() falha. Vemos que o valor expected, 0, é diferente do valor actual, 2.

      Depois disso, vemos a linha do nosso arquivo de teste em que o código falha. Neste caso, é a linha 10.

      Agora, vimos por conta própria que nosso teste falhará se esperarmos valores incorretos. Vamos alterar nosso caso de teste novamente para o valor correto. Primeiro, abra o arquivo:

      Em seguida, retire as linhas todos.add para que seu código se pareça com o seguinte:

      todos/index.test.js

      ...
      describe("integration test", function () {
          it("should be able to add and complete TODOs", function () {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Salve e saia do arquivo.

      Execute-o novamente para confirmar que ele foi aprovado sem possíveis falsos positivos:

      O resultado será o seguinte:

      Output

      ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

      Agora, melhoramos significativamente nossa resiliência do teste. Vamos prosseguir com o nosso teste de integração. O próximo passo é adicionar um novo item de AFAZERES no index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
          });
      });
      

      Após usar a função add(), confirmamos que temos agora um item de AFAZERES sendo gerenciado pelo nosso objeto todos com o strictEqual(). Nosso próximo teste confirma os dados em todos com o deepStrictEqual(). A função deepStrictEqual() testa recursivamente se nossos objetos esperado e real possuem as mesmas propriedades. Neste caso, ela testa se ambas as matrizes esperadas possuem um objeto do JavaScript dentro delas. Depois disso, verifica se seus objetos do JavaScript possuem as mesmas propriedades, ou seja, se ambas as suas propriedades title são "run code" e ambas as suas propriedades completed são false.

      Depois disso, completamos os testes restantes usando esses dois controles de igualdade, conforme necessário, pela adição das seguintes linhas em destaque:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
      
              todos.add("test everything");
              assert.strictEqual(todos.list().length, 2);
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: false },
                      { title: "test everything", completed: false }
                  ]
              );
      
              todos.complete("run code");
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: true },
                      { title: "test everything", completed: false }
                  ]
          );
        });
      });
      

      Salve e saia do arquivo.

      Nosso teste agora imita nosso teste manual. Com esses testes programáticos, não precisamos verificar o resultado continuamente se nossos testes forem aprovados quando os executarmos. Normalmente, deseja-se testar todos os aspectos de uso para garantir que o código seja testado corretamente.

      Vamos executar nosso teste com o npm test novamente para obter este resultado bastante conhecido:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

      Agora, você configurou um teste integrado com o framework do Mocha e a biblioteca assert.

      Vamos considerar uma situação em que compartilhamos nosso módulo com outros desenvolvedores, e, agora, eles estão nos dando feedback. Uma boa parte de nossos usuários iria gostar que a função complete() retornasse um erro se nenhum item de AFAZERES tivesse sido adicionado até agora. Vamos adicionar essa funcionalidade em nossa função complete().

      Abra o index.js no seu editor de texto:

      Adicione o que vem a seguir à função:

      todos/index.js

      ...
      complete(title) {
          if (this.todos.length === 0) {
              throw new Error("You have no TODOs stored. Why don't you add one first?");
          }
      
          let todoFound = false
          this.todos.forEach((todo) => {
              if (todo.title === title) {
                  todo.completed = true;
                  todoFound = true;
                  return;
              }
          });
      
          if (!todoFound) {
              throw new Error(`No TODO was found with the title: "${title}"`);
          }
      }
      ...
      

      Salve e saia do arquivo.

      Agora, vamos adicionar um novo teste para este novo recurso. Queremos verificar se, caso um objeto Todos que não possui itens seja chamado de completo, ele retornará nosso erro especial.

      Volte para o index.test.js:

      No final do arquivo, adicione o seguinte código:

      todos/index.test.js

      ...
      describe("complete()", function() {
          it("should fail if there are no TODOs", function() {
              let todos = new Todos();
              const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
      
              assert.throws(() => {
                  todos.complete("doesn't exist");
              }, expectedError);
          });
      });
      

      Usamos o describe() e o it() assim como anteriormente. Nosso teste começa com a criação de um novo objeto todos. Depois disso, definimos o erro que estamos esperando receber ao chamarmos a função complete().

      Em seguida, usamos a função throws() do módulo assert. Essa função foi criada para que possamos verificar os erros que são emitidos em nosso código. Seu primeiro argumento é uma função que contém o código que emite o erro. O segundo argumento é o erro que estamos esperando receber.

      Em seu terminal, execute novamente os testes com o npm test e você verá agora o seguinte resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

      Este resultado destaca o benefício e o porquê de realizarmos testes automatizados com o Mocha e o assert. Como nossos testes têm um script, toda vez que executamos o npm test, verificamos que todos os nossos testes estão sendo aprovados. Não precisamos verificar manualmente se o outro código ainda está funcionando; sabemos que está, porque o teste que fizemos ainda foi aprovado.

      Até agora, nossos testes verificaram os resultados de código síncrono. Vamos ver como precisaríamos adaptar nossos hábitos de teste recém-formados para trabalhar com código assíncrono.

      Passo 4 — Testando código assíncrono

      Uma das funcionalidades que queremos em nosso módulo de AFAZERES é um recurso de exportação em CSV. Isso imprimirá todos os AFAZERES que temos armazenados, junto com o status finalizado para um arquivo. Isso exige que usemos o módulo fs — um módulo integrado do Node.js para trabalhar com o sistema de arquivos.

      Escrever em um arquivo é uma operação assíncrona. Há várias maneiras de gravar em um arquivo no Node.js. Podemos usar callbacks, promessas, ou as palavras-chave async/await. Nesta seção, veremos como gravar testes para esses diferentes métodos.

      Callbacks

      Uma função callback é usada como um argumento para uma função assíncrona. Ela é chamada quando a operação assíncrona é concluída.

      Vamos adicionar uma função à nossa classe Todos chamada saveToFile(). Essa função construirá uma string, percorrendo em loop todos os nossos itens de AFAZERES e gravando a string em um arquivo.

      Abra o seu arquivo index.js:

      Neste arquivo, adicione o código em destaque a seguir:

      todos/index.js

      const fs = require('fs');
      
      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
              this.todos.push(todo);
          }
      
          complete(title) {
              if (this.todos.length === 0) {
                  throw new Error("You have no TODOs stored. Why don't you add one first?");
              }
      
              let todoFound = false
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      
          saveToFile(callback) {
              let fileContents = 'Title,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Primeiro, precisamos importar o módulo fs para o nosso arquivo. Depois disso, adicionamos nossa nova função saveToFile(). Nossa função recebe uma função de callback que será usada assim que a operação de gravação do arquivo for concluída. Nessa função, criamos uma variável fileContents que armazena toda a string que queremos que seja salva como um arquivo. Ela é inicializada com os cabeçalhos do CSV. Depois disso, percorremos em loop cada item de AFAZERES com o método forEach() da matriz interna. À medida que iteramos, adicionamos as propriedades title e completed dos objetos todos individuais.

      Por fim, usamos o módulo fs para gravar o arquivo com a função writeFile(). Nosso primeiro argumento é o nome do arquivo: todos.csv. O segundo é o conteúdo do arquivo, sendo neste caso, nossa variável fileContents. Nosso último argumento é a nossa função callback, que lida com quaisquer erros de gravação de arquivos.

      Salve e saia do arquivo.

      Vamos agora escrever um teste para a nossa função saveToFile(). Nosso teste fará duas coisas: confirmar se o arquivo existe em primeiro lugar e, em seguida, verificar se ele tem o conteúdo correto.

      Abra o arquivo index.test.js:

      Vamos começar carregando o módulo fs no topo do arquivo, pois o usaremos para ajudar a testar nossos resultados:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      const fs = require('fs');
      ...
      

      Agora, no final do arquivo, vamos adicionar nosso novo caso de teste:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function(done) {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.saveToFile((err) => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Assim como anteriormente, usamos o describe() para agrupar nosso teste separadamente dos outros, uma vez que ele envolve uma nova funcionalidade. A função it() é ligeiramente diferente de nossas outras. Normalmente, a função callback que usamos não tem argumentos. Desta vez, temos done como um argumento. Precisamos desse argumento ao testar as funções com callbacks. A função callback done() é usada pelo Mocha para dizer a ele quando uma função assíncrona é concluída.

      Todas as funções callback que estão sendo testadas no Mocha devem chamar o callback done() Caso contrário, o Mocha nunca saberia quando a função foi concluída e ficaria preso à espera de um sinal.

      Continuando, criamos nossa instância Todos e adicionamos um único item a ela. Em seguida, chamamos a função saveToFile() com um call que captura um erro de gravação de arquivos. Note como nosso teste para essa função reside no callback. Caso nosso código de teste estivesse fora do callback, ele falharia, enquanto o código fosse chamado antes da gravação do arquivo terminar.

      Em nossa função callback, verificamos primeiro se o nosso arquivo existe:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      A função fs.existsSync() retorna true caso o caminho do arquivo em seu argumento exista, false caso contrário.

      Nota: as funções do módulo fs são assíncronas por padrão. No entanto, para as funções chave, foram criadas contrapartes síncronas. Este teste é mais simples ao usar funções síncronas, pois não precisamos aninhar o código assíncrono para garantir que ele funcione. No módulo fs, as funções síncronas geralmente têm seus nomes terminados com "Sync".

      Depois disso, criamos uma variável para armazenar nosso valor esperado:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Usamos o readFileSync() do módulo fs para ler o arquivo de maneira síncrona:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Agora, provisionamos o readFileSync() com o caminho correto para o arquivo: todos.csv. À medida que o readFileSync() retorna um objeto Buffer, que armazena dados binários, usamos seu método toString() para que possamos comparar seu valor com a string que esperamos que tenha sido salva.

      Assim como anteriormente, usamos o strictEqual, do módulo assert, para fazer uma comparação:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Terminamos nosso teste chamando o callback done(), garantindo que o Mocha saiba quando parar de testar esse caso:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Fornecemos o objeto err ao done(), para que o Mocha possa reprovar o teste caso tenha ocorrido um erro.

      Salve e saia do index.test.js.

      Vamos executar este teste com o npm test, assim como anteriormente. Seu console exibirá este resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

      Agora, você testou sua primeira função assíncrona com o Mocha usando callbacks. Mas no momento em que este tutorial é escrito, as Promessas são mais prevalentes que callbacks em novos códigos do Node.js, como explicado em nosso artigo Como escrever um código assíncrono em Node.js. Em seguida, vamos aprender também a como testá-los com o Mocha.

      Promessas

      Uma Promessa é um objeto do JavaScript que retornará, eventualmente, um valor. Quando uma Promessa é bem-sucedida, ela é resolvida. Quando encontra um erro, ela é rejeitada.

      Vamos modificar a função saveToFile(), para que ela utilize Promessas, em vez de callbacks. Abra o index.js:

      Primeiro, precisamos alterar a forma como o módulo fs é carregado. No seu arquivo index.js, modifique a declaração require() no topo do arquivo, para que se pareça com isto:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Acabamos de importar o módulo fs que utiliza Promessas, em vez de callbacks. Agora, precisamos fazer algumas alterações no saveToFile(), para que ele funcione com Promessas.

      No seu editor de texto, faça as seguintes alterações à função saveToFile() para remover os callbacks:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      A primeira diferença é que nossa função já não aceita qualquer argumento. Com Promessas, não precisamos de uma função callback. A segunda mudança diz respeito à forma como o arquivo é gravado. Agora, retornamos o resultado da promessa writeFile().

      Salve e feche o index.js.

      Vamos agora adaptar nosso teste, para que ele funcione com Promessas. Abra o index.test.js:

      Altere o teste saveToFile(), substituindo por isto:

      todos/index.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function() {
              let todos = new Todos();
              todos.add("save a CSV");
              return todos.saveToFile().then(() => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      A primeira mudança que precisamos fazer é remover o callback done() dos seus argumentos. Caso o Mocha passe o argumento done(), ele precisa ser chamado, ou emitirá um erro assim:

      1) saveToFile()
             should save a single TODO:
           Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
            at listOnTimeout (internal/timers.js:536:17)
            at processTimers (internal/timers.js:480:7)
      

      Ao testar Promessas, não inclua o callback done() no it().

      Para testar nossa promessa, precisamos colocar nosso código de asserção na função then(). Note que retornamos essa promessa no teste e que não temos uma função catch() para capturar quando a Promessa é rejeitada.

      Retornaremos a promessa, para que quaisquer erros que forem lançados na função then() sejam borbulhados até a função it(). Caso os erros não sejam borbulhados, o Mocha não reprovará o caso de teste. Ao testar Promessas, é necessário usar o return na Promessa que está sendo testada. Caso contrário, existe o risco de se obter um falso positivo.

      Também omitimos a cláusula catch(), porque o Mocha pode detectar quando uma promessa é rejeitada. Caso tenha sido rejeitada, ele reprova automaticamente o teste.

      Agora que temos nosso teste funcionando, salve e saia do arquivo. Em seguida, execute o Mocha com o npm test para confirmar se recebemos um resultado bem-sucedido:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

      Alteramos nosso código e teste para usar Promessas e agora sabemos que ele funciona. No entanto, os padrões assíncronos mais recentes utilizam as palavras-chave async/await. Isso é feito para que não precisemos criar várias funções then() para lidar com resultados bem-sucedidos. Vamos ver como testar com async/await.

      async/await

      As palavras-chave async/await tornam o trabalho com as Promessas menos prolixo. Assim que definimos uma função como assíncrona com a palavra-chave async, podemos obter quaisquer resultados futuros nessa função com a palavra-chave await. Desta maneira, podemos usar as Promessas sem precisar usar as funções then() ou catch().

      Podemos simplificar nosso teste saveToFile(), que é baseado em promessas, com async/await. No seu editor de texto, faça essas pequenas edições no teste saveToFile(), dentro de index.test.js:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", async function() {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      A primeira mudança é que a função utilizada pela função it() agora tem a palavra-chave async quando é definida. Isso nos permite usar a palavra-chave await dentro do seu corpo.

      A segunda mudança é encontrada quando chamamos saveToFile(). A palavra-chave await é usada antes de ser chamada. Agora, o Node.js sabe que precisa esperar até que essa função seja resolvida antes de continuar o teste.

      Agora, o código da nossa função é mais fácil de se ler, uma vez que mudamos o código que estava na função then() para o corpo da função it(). Executar este código com o npm test produz este resultado:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

      Agora, podemos testar as funções assíncronas usando qualquer um dos três paradigmas assíncronos adequadamente.

      Abordamos bastante conteúdo sobre realizar testes com o código síncrono e assíncrono com o Mocha. Em seguida, vamos ir um pouco mais fundo em algumas outras funcionalidades que o Mocha oferece para melhorar nossa experiência de teste. Em particular, como os ganchos podem mudar os ambientes de teste.

      Passo 5 — Usando hooks (ganchos) para melhorar os casos de teste

      Os ganchos são uma funcionalidade útil do Mocha que nos permite configurar o ambiente antes e após um teste. Normalmente, adicionamos ganchos dentro de um bloco de função describe(), já que eles possuem lógica de configuração e desmontagem específicos para alguns casos de teste.

      O Mocha provisiona quatro ganchos que podemos usar em nossos testes:

      • before: esse gancho é executado uma vez antes do início do teste.
      • beforeEach: este gancho é executado antes de todos os casos de teste.
      • after: este gancho é executado uma vez após o último caso de teste é concluído.
      • afterEach: este gancho é executado após todos os casos de teste.

      Quando testamos uma função ou recurso várias vezes, os ganchos são úteis, uma vez que nos permitem separar o código de configuração do teste (assim como na criação do objeto todos) do código de declaração do teste.

      Para ver o valor dos ganchos, vamos adicionar mais testes ao nosso bloco de teste saveToFile().

      Apesar de termos confirmado que podemos salvar nossos itens de AFAZERES em um arquivo, salvamos apenas um item. Além disso, o item não foi marcado como concluído. Vamos adicionar mais testes para ter certeza de que os vários aspectos do nosso módulo funcionam.

      Primeiro, vamos adicionar um segundo teste para confirmar que nosso arquivo é salvo corretamente quando concluímos um item de AFAZERES. Abra seu arquivo index.test.js no editor de texto:

      Troque o último teste pelo seguinte:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          it("should save a single TODO", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.complete("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      O teste é semelhante ao que tínhamos anteriormente. As principais diferenças são que chamamos a função complete() antes de chamar saveToFile(), e que nosso expectedFileContents tem agora agora true, ao invés de false para o valor completed da coluna.

      Salve e saia do arquivo.

      Vamos executar nosso novo teste, e todos os outros, com o npm test:

      Isso resultará no seguinte:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

      Ele está funcionando conforme esperado. No entanto, há espaço para melhorias. Ambos precisam instanciar um objeto Todos no início do teste. À medida que adicionamos mais casos de teste, isso torna-se rapidamente repetitivo e um desperdício de memória. Além disso, sempre que executamos o teste, ele cria um arquivo. Isso pode ser confundido com o resultado real por alguém menos familiarizado com o módulo. Seria bom se limpássemos nossos arquivos do resultado após os testes.

      Vamos fazer essas melhorias usando ganchos de teste. Vamos usar o gancho beforeEach() para configurar nosso acessório de teste dos itens de AFAZERES. Um acessório de teste é qualquer estado consistente usado em um teste. Em nosso caso, nosso acessório de teste é um novo objeto todos que tem um item de AFAZERES já adicionado nele. Depois disso, usaremos o afterEach() para remover o arquivo criado pelo teste.

      No index.test.js, faça as seguintes alterações no seu último teste em saveToFile():

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          beforeEach(function () {
              this.todos = new Todos();
              this.todos.add("save a CSV");
          });
      
          afterEach(function () {
              if (fs.existsSync("todos.csv")) {
                  fs.unlinkSync("todos.csv");
              }
          });
      
          it("should save a single TODO without error", async function () {
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync("todos.csv"), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              this.todos.complete("save a CSV");
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Vamos detalhar todas as alterações que fizemos. Adicionamos um bloco de beforeEach() no bloco de teste:

      todos/index.test.js

      ...
      beforeEach(function () {
          this.todos = new Todos();
          this.todos.add("save a CSV");
      });
      ...
      

      Essas duas linhas de código criam um novo objeto Todos que ficará disponível em cada um dos nossos testes. Com o Mocha, o objeto this em beforeEach() refere-se ao mesmo objeto this no it(). O this é o mesmo para todos os blocos de código dentro do bloco describe(). Para obter mais informações sobre o this, consulte nosso tutorial Entendendo this, bind, call e apply no JavaScript.

      Este poderoso compartilhamento de contexto é o motivo pelo qual podemos criar rapidamente os acessórios de teste que funcionam para ambos os nossos testes.

      Depois disso, limparemos nosso arquivo CSV na função afterEach():

      todos/index.test.js

      ...
      afterEach(function () {
          if (fs.existsSync("todos.csv")) {
              fs.unlinkSync("todos.csv");
          }
      });
      ...
      

      Caso nosso teste tenha falhado, então ele pode não ter criado um arquivo. Por esse motivo, verificamos se o arquivo existe antes de usar a função unlinkSync() para excluí-lo.

      As alterações remanescentes trocam a referência dos todos, que foram criados anteriormente na função it(), para this.todos, que está disponível no contexto do Mocha. Também excluímos as linhas que instanciaram os todos anteriormente nos casos de teste individuais.

      Agora, vamos executar este arquivo para confirmar se nossos testes ainda funcionam. Insira o npm teste em seu terminal para obter:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

      Os resultados são os mesmos e, como um benefício, reduzimos ligeiramente o tempo de configuração dos novos testes para a função saveToFile(). Além disso, encontramos uma solução para o arquivo CSV residual.

      Conclusão

      Neste tutorial, você escreveu um módulo do Node.js para gerenciar os itens de AFAZERES e testou o código manualmente usando o REPL do Node.js. Depois disso, criou um arquivo de teste e usou o framework do Mocha para executar testes automatizados. Com o módulo assert, você foi capaz de verificar se seu código funciona. Você também testou funções síncronas e assíncronas com o Mocha. Por fim, criou ganchos com o Mocha que fazem com que escrever vários casos de teste relacionados seja muito mais legível e sustentável.

      Equipado com este conhecimento, desafio você a escrever testes para novos módulos do Node.js que está criando. Consegue pensar nas entradas e resultados da sua função e escrever seu teste antes de escrever seu código?

      Caso queira mais informações sobre o framework de teste do Mocha, confira a documentação oficial do Mocha. Caso queira continuar aprendendo sobre o Node.js, volte para a página da série Como codificar em Node.js.



      Source link

      Como Testar Seu Deployment Ansible com InSpec e Kitchen


      O autor escolheu a Diversity in Tech Fund para receber uma doação como parte do programa Write for DOnations.

      Introdução

      O InSpec é um framework open-source de auditoria e teste automatizado usado para descrever e testar preocupações, recomendações ou requisitos regulatórios. Ele foi projetado para ser inteligível e independente de plataforma. Os desenvolvedores podem trabalhar com o InSpec localmente ou usando SSH, WinRM ou Docker para executar testes, portanto, é desnecessário instalar quaisquer pacotes na infraestrutura que está sendo testada.

      Embora com o InSpec você possa executar testes diretamente em seus servidores, existe um potencial de erro humano que poderia causar problemas em sua infraestrutura. Para evitar esse cenário, os desenvolvedores podem usar o Kitchen para criar uma máquina virtual e instalar um sistema operacional de sua escolha nas máquinas em que os testes estão sendo executados. O Kitchen é um executor de testes, ou ferramenta de automação de teste, que permite testar o código de infraestrutura em uma ou mais plataformas isoladas. Ele também suporta muitos frameworks de teste e é flexível com uma arquitetura de plug-in de driver para várias plataformas, como Vagrant, AWS, DigitalOcean, Docker, LXC containers, etc.

      Neste tutorial, você escreverá testes para seus playbooks Ansible em execução em um Droplet Ubuntu 18.04 da DigitalOcean. Você usará o Kitchen como executor de teste e o InSpec para escrever os testes. No final deste tutorial, você poderá testar o deploy do seu playbook Ansible.

      Pré-requisitos

      Antes de começar com este guia, você precisará de uma conta na DigitalOcean além do seguinte:

      Passo 1 — Configurando e Inicializando o Kitchen

      Você instalou o ChefDK como parte dos pré-requisitos que vem empacotados com o kitchen. Neste passo, você configurará o Kitchen para se comunicar com a DigitalOcean.

      Antes de inicializar o Kitchen, você criará e se moverá para um diretório de projeto. Neste tutorial, o chamaremos de ansible_testing_dir.

      Execute o seguinte comando para criar o diretório:

      • mkdir ~/ansible_testing_dir

      E então passe para ele:

      Usando o gem instale o pacote kitchen-digitalocean em sua máquina local. Isso permite que você diga ao kitchen para usar o driver da DigitalOcean ao executar testes:

      • gem install kitchen-digitalocean

      No diretório do projeto, você executará o comando kitchen init especificando ansible_playbook como o provisionador e digitalocean como o driver ao inicializar o Kitchen:

      • kitchen init --provisioner=ansible_playbook --driver=digitalocean

      Você verá a seguinte saída:

      Output

      create kitchen.yml create chefignore create test/integration/default

      Isso criou o seguinte no diretório do projeto:

      • test/integration/default é o diretório no qual você salvará seus arquivos de teste.

      • chefignore é o arquivo que você usaria para garantir que certos arquivos não sejam carregados para o Chef Infra Server, mas você não o usará neste tutorial.

      • kitchen.yml é o arquivo que descreve sua configuração de teste: o que você deseja testar e as plataformas de destino.

      Agora, você precisa exportar suas credenciais da DigitalOcean como variáveis de ambiente para ter acesso para criar Droplets a partir da sua CLI. Primeiro, inicie com seu token de acesso da DigitalOcean executando o seguinte comando:

      • export DIGITALOCEAN_ACCESS_TOKEN="SEU_TOKEN_DE_ACESSO_DIGITALOCEAN"

      Você também precisa obter seu número de ID da chave SSH; note que SEU_ID_DE_CHAVE_SSH_DIGITALOCEAN deve ser o ID numérico da sua chave SSH, não o nome simbólico. Usando a API da DigitalOcean, você pode obter o ID numérico de suas chaves com o seguinte comando:

      • curl -X GET https://api.digitalocean.com/v2/account/keys -H "Authorization: Bearer $DIGITALOCEAN_ACCESS_TOKEN"

      Após este comando, você verá uma lista de suas chaves SSH e metadados relacionados. Leia a saída para encontrar a chave correta e identificar o número de ID nela:

      Output

      ... {"id":seu-ID-numérico,"fingerprint":"fingerprint","public_key":"ssh-rsa sua-chave-ssh","name":"nome-da-sua-chave-ssh" ...

      Nota: Se você deseja tornar sua saída mais legível para obter seus IDs numéricos, você pode encontrar e baixar o jq com base no seu sistema operacional na página de download do jq. Agora, você pode executar o comando anterior fazendo um pipe para o jq da seguinte maneira:

      • curl -X GET https://api.digitalocean.com/v2/account/keys -H "Authorization: Bearer $DIGITALOCEAN_ACCESS_TOKEN" | jq

      Você verá as informações da chave SSH formatadas de forma semelhante a:

      Output

      { "ssh_keys": [ { "id": ID_DA_SUA_CHAVE_SSH, "fingerprint": "2f:d0:16:6b", "public_key": "ssh-rsa AAAAB3NzaC1yc2 example@example.local", "name": "sannikay" } ], }

      Depois de identificar seus IDs numéricos de SSH, exporte-os com o seguinte comando:

      • export DIGITALOCEAN_SSH_KEY_IDS="SEU_ID_DE_CHAVE_SSH_DIGITALOCEAN"

      Você inicializou o kitchen e configurou as variáveis de ambiente para suas credenciais da DigitalOcean. Agora você vai criar e executar testes em seus Droplets diretamente da linha de comando.

      Passo 2 — Criando o Playbook Ansible

      Neste passo, você criará um playbook e roles (funções) que configurará o Nginx e o Node.js no Droplet criado pelo kitchen no próximo passo. Seus testes serão executados no playbook para garantir que as condições especificadas no playbook sejam atendidas.

      Para começar, crie um diretório roles para as roles ou funções Nginx e Node.js:

      • mkdir -p roles/{nginx,nodejs}/tasks

      Isso criará uma estrutura de diretórios da seguinte maneira:

      roles
      ├── nginx
      │   └── tasks
      └── nodejs
          └── tasks
      

      Agora, crie um arquivo main.yml no diretório roles/nginx/tasks usando o seu editor preferido:

      • nano roles/nginx/tasks/main.yml

      Neste arquivo, crie uma tarefa ou task que configura e inicia o Nginx adicionando o seguinte conteúdo:

      roles/nginx/tasks/main.yml

      ---
      - name: Update cache repositories and install Nginx
        apt:
          name: nginx
          update_cache: yes
      
      - name: Change nginx directory permission
        file:
          path: /etc/nginx/nginx.conf
          mode: 0750
      
      - name: start nginx
        service:
          name: nginx
          state: started
      

      Depois de adicionar o conteúdo, salve e saia do arquivo.

      Em roles/nginx/tasks/main.yml você define uma tarefa que atualizará o repositório de cache do seu Droplet, o que equivale a executar o comando apt update manualmente em um servidor. Essa tarefa também altera as permissões do arquivo de configuração do Nginx e inicia o serviço Nginx.

      Você também criará um arquivo main.yml em roles/nodejs/tasks para definir uma tarefa que configure o Node.js.

      • nano roles/nodejs/tasks/main.yml

      Adicione as seguintes tarefas a este arquivo:

      roles/nodejs/tasks/main.yml

      ---
      - name: Update caches repository
        apt:
          update_cache: yes
      
      - name: Add gpg key for NodeJS LTS
        apt_key:
          url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key"
          state: present
      
      - name: Add the NodeJS LTS repo
        apt_repository:
          repo: "deb https://deb.nodesource.com/node_{{ NODEJS_VERSION }}.x {{ ansible_distribution_release }} main"
          state: present
          update_cache: yes
      
      - name: Install Node.js
        apt:
          name: nodejs
          state: present
      
      

      Salve e saia do arquivo quando terminar.

      Em roles/nodejs/tasks/main.yml, você primeiro define uma tarefa que atualizará o repositório de cache do seu Droplet. Em seguida, na próxima tarefa, você adiciona a chave GPG para o Node.js, que serve como um meio de verificar a autenticidade do repositório apt do Node.js. As duas tarefas finais adicionam o repositório apt do Node.js e o instalam.

      Agora você definirá suas configurações do Ansible, como variáveis, a ordem em que você deseja que suas roles sejam executadas e configurações de privilégios de superusuário. Para fazer isso, você criará um arquivo chamado playbook.yml, que serve como um entry point para o Kitchen. Quando você executa seus testes, o Kitchen inicia no seu arquivo playbook.yml e procura as roles a serem executadas, que são seus arquivos roles/nginx/tasks/main.yml e roles/nodejs/tasks/main.yml.

      Execute o seguinte comando para criar o playbook.yml:

      Adicione o seguinte conteúdo ao arquivo:

      ansible_testing_dir/playbook.yml

      ---
       - hosts: all
         become: true
         remote_user: ubuntu
         vars:
          NODEJS_VERSION: 8
      

      Salve e saia do arquivo.

      Você criou as roles do playbook do Ansible com as quais executará seus testes para garantir que as condições especificadas no playbook sejam atendidas.

      Passo 3 — Escrevendo Seus Testes InSpec

      Neste passo, você escreverá testes para verificar se o Node.js está instalado no seu Droplet. Antes de escrever seu teste, vejamos o formato de um exemplo de teste InSpec. Como em muitos frameworks de teste, o código InSpec se assemelha a uma linguagem natural. O InSpec possui dois componentes principais, o assunto a ser examinado e o estado esperado desse assunto:

      block A

      describe '<entity>' do
        it { <expectation> }
      end
      

      Em block A, as palavras-chave do e end definem um bloco ou block. A palavra-chave describe é comumente conhecida como conjuntos ou suites de testes, que contêm casos de teste. A palavra-chave it é usada para definir os casos de teste.

      <entity> é o assunto que você deseja examinar, por exemplo, um nome de pacote, serviço, arquivo ou porta de rede. O <expectation> especifica o resultado desejado ou o estado esperado, por exemplo, o Nginx deve ser instalado ou deve ter uma versão específica. Você pode verificar a documentação da InSpec DSL para aprender mais sobre a linguagem InSpec.

      Outro exemplo de bloco de teste InSpec:

      block B

      control 'Pode ser qualquer coisa única' do  
        impact 0.7                         
        title 'Um título inteligível'     
        desc  'Uma descrição opcional'
        describe '<entity>' do             
          it { <expectation> }
        end
      end
      

      A diferença entre o bloco A e o bloco B é o bloco control. O bloco control é usado como um meio de controle regulatório, recomendação ou requisito. O bloco control tem um nome; geralmente um ID único, metadados como desc, title, impact e, finalmente, agrupam blocos describe relacionados para implementar as verificações.

      desc, title, e impact definem metadados que descrevem completamente a importância do controle, seu objetivo, com uma descrição sucinta e completa. impact define um valor numérico que varia de 0.0 a 1.0 onde 0.0 a <0.01 é classificado como sem impacto, 0.01 a <0.4 é classificado como baixo impacto, 0.4 a <0.7 é classificado como médio impacto, 0,7 a <0,9 é classificado como alto impacto, 0,9 a 1,0 é classificado como controle crítico.

      Agora, vamos implementar um teste. Usando a sintaxe do bloco A, você usará o recurso package do InSpec para testar se o Node.js está instalado no sistema. Você irá criar um arquivo chamado sample.rb em seu diretório test/integration/default para seus testes.

      Crie o sample.rb:

      • nano test/integration/default/sample.rb

      Adicione o seguinte ao seu arquivo:

      test/integration/default/sample.rb

      describe package('nodejs') do
        it { should be_installed }
      end
      

      Aqui seu teste está usando o recurso package para verificar se o node.js está instalado.

      Salve e saia do arquivo quando terminar.

      Para executar este teste, você precisa editar kitchen.yml para especificar o playbook que você criou anteriormente e para adicionar às suas configurações.

      Abra seu arquivo kitchen.yml:

      • nano ansible_testing_dir/kitchen.yml

      Substitua o conteúdo de kitchen.yml com o seguinte:

      ansible_testing_dir/kitchen.yml

      ---
      driver:
        name: digitalocean
      
      provisioner:
        name: ansible_playbook
        hosts: test-kitchen
        playbook: ./playbook.yml
      
      verifier:
        name: inspec
      
      platforms:
        - name: ubuntu-18
          driver_config:
            ssh_key: CAMINHO_PARA_SUA_CHAVE_PRIVADA_SSH
            tags:
              - inspec-testing
            region: fra1
            size: 1gb
            private_networking: false
          verifier:
            inspec_tests:
              - test/integration/default
      suites:
        - name: default
      
      

      As opções de platform incluem o seguinte:

      • name: A imagem que você está usando.
      • driver_config: A configuração do seu Droplet da DigitalOcean. Você está especificando as seguintes opções para driver_config:

        • ssh_key: Caminho para SUA_CHAVE_SSH_PRIVADA. Sua SUA_CHAVE_SSH_PRIVADA está localizada no diretório que você especificou ao criar sua chave ssh.
        • tags: As tags associadas ao seu Droplet.
        • region: A region ou região onde você deseja que seu Droplet seja hospedado.
        • size: A memória que você deseja que seu Droplet tenha.
      • verifier: Isso define que o projeto contém testes InSpec.

        • A parte do inspec_tests especifica que os testes existem no diretório test/integration/default do projeto.

      Observe que name e region usam abreviações. Você pode verificar na documentação do test-kitchen as abreviações que você pode usar.

      Depois de adicionar sua configuração, salve e saia do arquivo.

      Execute o comando kitchen test para executar o teste. Isso verificará se o Node.js está instalado — ele falhará propositalmente, porque você atualmente não possui a role Node.js no seu arquivo playbook.yml:

      Você verá uma saída semelhante à seguinte:

      Output: failing test results

      -----> Starting Kitchen (v1.24.0) -----> Cleaning up any prior instances of <default-ubuntu-18> -----> Destroying <default-ubuntu-18>... DigitalOcean instance <145268853> destroyed. Finished destroying <default-ubuntu-18> (0m2.63s). -----> Testing <default-ubuntu-18> -----> Creating <default-ubuntu-18>... DigitalOcean instance <145273424> created. Waiting for SSH service on 138.68.97.146:22, retrying in 3 seconds [SSH] Established (ssh ready) Finished creating <default-ubuntu-18> (0m51.74s). -----> Converging <default-ubuntu-18>... $$$$$$ Running legacy converge for 'Digitalocean' Driver -----> Installing Chef Omnibus to install busser to run tests PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] PLAY RECAP ********************************************************************* localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Downloading files from <default-ubuntu-18> Finished converging <default-ubuntu-18> (0m55.05s). -----> Setting up <default-ubuntu-18>... $$$$$$ Running legacy setup for 'Digitalocean' Driver Finished setting up <default-ubuntu-18> (0m0.00s). -----> Verifying <default-ubuntu-18>... Loaded tests from {:path=>". ansible_testing_dir.test.integration.default"} Profile: tests from {:path=>"ansible_testing_dir/test/integration/default"} (tests from {:path=>"ansible_testing_dir.test.integration.default"}) Version: (not specified) Target: ssh://root@138.68.97.146:22 System Package nodejs × should be installed expected that System Package nodejs is installed Test Summary: 0 successful, 1 failure, 0 skipped >>>>>> ------Exception------- >>>>>> Class: Kitchen::ActionFailed >>>>>> Message: 1 actions failed. >>>>>> Verify failed on instance <default-ubuntu-18>. Please see .kitchen/logs/default-ubuntu-18.log for more details >>>>>> ---------------------- >>>>>> Please see .kitchen/logs/kitchen.log for more details >>>>>> Also try running `kitchen diagnose --all` for configuration 4.54s user 1.77s system 5% cpu 2:02.33 total

      A saída informa que seu teste está falhando porque você não possui o Node.js instalado no Droplet que você provisionou com o kitchen. Você corrigirá seu teste adicionando a role nodejs ao seu arquivo playbook.yml e executará o teste novamente.

      Edite o arquivo playbook.yml para incluir a role nodejs:

      Adicione as seguintes linhas destacadas ao seu arquivo:

      ansible_testing_dir/playbook.yml

      ---
       - hosts: all
         become: true
         remote_user: ubuntu
         vars:
          NODEJS_VERSION: 8
      
         roles:
          - nodejs
      

      Salve e feche o arquivo.

      Agora, você executará novamente o teste usando o comando kitchen test:

      Você verá a seguinte saída:

      Output

      ...... Target: ssh://root@46.101.248.71:22 System Package nodejs ✔ should be installed Test Summary: 1 successful, 0 failures, 0 skipped Finished verifying <default-ubuntu-18> (0m4.89s). -----> Destroying <default-ubuntu-18>... DigitalOcean instance <145512952> destroyed. Finished destroying <default-ubuntu-18> (0m2.23s). Finished testing <default-ubuntu-18> (2m49.78s). -----> Kitchen is finished. (2m55.14s) 4.86s user 1.77s system 3% cpu 2:56.58 total

      Seu teste agora passa porque você tem o Node.js instalado usando a role nodejs.

      Aqui está um resumo do que o Kitchen está fazendo em Test Action:

      • Destrói o Droplet se ele existir
      • Cria o Droplet
      • Converge o Droplet
      • Verifica o Droplet com o InSpec
      • Destrói o Droplet

      O Kitchen interromperá a execução em seu Droplet se encontrar algum problema. Isso significa que, se o seu playbook do Ansible falhar, o InSpec não será executado e o seu Droplet não será destruído. Isso permite que você inspecione o estado da instância e corrija quaisquer problemas. O comportamento da ação final de destruição pode ser substituído, se desejado. Verifique a ajuda da CLI para a flag --destroy executando o comando kitchen help test.

      Você escreveu seus primeiros testes e os executou no seu playbook com uma instância falhando antes de corrigir o problema. Em seguida, você estenderá seu arquivo de teste.

      Passo 4 — Adicionando Casos de Teste

      Neste passo, você adicionará mais casos de teste ao seu arquivo de teste para verificar se os módulos do Nginx estão instalados no seu Droplet e se o arquivo de configuração tem as permissões corretas.

      Edite seu arquivo sample.rb para adicionar mais casos de teste:

      • nano test/integration/default/sample.rb

      Adicione os seguintes casos de teste ao final do arquivo:

      test/integration/default/sample.rb

      . . .
      control 'nginx-modules' do
        impact 1.0
        title 'NGINX modules'
        desc 'The required NGINX modules should be installed.'
        describe nginx do
          its('modules') { should include 'http_ssl' }
          its('modules') { should include 'stream_ssl' }
          its('modules') { should include 'mail_ssl' }
        end
      end
      
      control 'nginx-conf' do
        impact 1.0
        title 'NGINX configuration'
        desc 'The NGINX config file should owned by root, be writable only by owner, and not writeable or and readable by others.'
        describe file('/etc/nginx/nginx.conf') do
          it { should be_owned_by 'root' }
          it { should be_grouped_into 'root' }
          it { should_not be_readable.by('others') }
          it { should_not be_writable.by('others') }
          it { should_not be_executable.by('others') }
        end
      end
      

      Esses casos de teste verificam se os módulos nginx-modules no seu Droplet incluem http_ssl, stream_ssl e mail_ssl. Você também está verificando as permissões do arquivo /etc/nginx/nginx.conf.

      Você está usando as palavras-chave it e its para definir seu teste. A palavra-chave its é usada apenas para acessar propriedades de resources. Por exemplo, modules é uma propriedade de nginx.

      Salve e saia do arquivo depois de adicionar os casos de teste.

      Agora execute o comando kitchen test para testar novamente:

      Você verá a seguinte saída:

      Output

      ... Target: ssh://root@104.248.131.111:22 ↺ nginx-modules: NGINX modules ↺ The `nginx` binary not found in the path provided. × nginx-conf: NGINX configuration (2 failed) × File /etc/nginx/nginx.conf should be owned by "root" expected `File /etc/nginx/nginx.conf.owned_by?("root")` to return true, got false × File /etc/nginx/nginx.conf should be grouped into "root" expected `File /etc/nginx/nginx.conf.grouped_into?("root")` to return true, got false ✔ File /etc/nginx/nginx.conf should not be readable by others ✔ File /etc/nginx/nginx.conf should not be writable by others ✔ File /etc/nginx/nginx.conf should not be executable by others System Package nodejs ✔ should be installed Profile Summary: 0 successful controls, 1 control failure, 1 control skipped Test Summary: 4 successful, 2 failures, 1 skipped

      Você verá que alguns dos testes estão falhando. Você irá corrigi-los adicionando a role nginx ao seu arquivo playbook e executando novamente o teste. No teste que falhou, você está verificando módulos nginx e permissões de arquivo que não estão presentes atualmente no seu servidor.

      Abra seu arquivo playbook.yml:

      • nano ansible_testing_dir/playbook.yml

      Adicione a seguinte linha destacada às suas roles:

      ansible_testing_dir/playbook.yml

      ---
      - hosts: all
        become: true
        remote_user: ubuntu
        vars:
        NODEJS_VERSION: 8
      
        roles:
        - nodejs
        - nginx
      

      Salve e feche o arquivo quando terminar.

      Em seguida, execute seus testes novamente:

      Você verá a seguinte saída:

      Output

      ... Target: ssh://root@104.248.131.111:22 ✔ nginx-modules: NGINX version ✔ Nginx Environment modules should include "http_ssl" ✔ Nginx Environment modules should include "stream_ssl" ✔ Nginx Environment modules should include "mail_ssl" ✔ nginx-conf: NGINX configuration ✔ File /etc/nginx/nginx.conf should be owned by "root" ✔ File /etc/nginx/nginx.conf should be grouped into "root" ✔ File /etc/nginx/nginx.conf should not be readable by others ✔ File /etc/nginx/nginx.conf should not be writable by others ✔ File /etc/nginx/nginx.conf should not be executable by others System Package nodejs ✔ should be installed Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped Test Summary: 9 successful, 0 failures, 0 skipped

      Depois de adicionar a role nginx ao playbook, todos os seus testes agora passam. A saída mostra que os módulos http_ssl, stream_ssl e mail_ssl estão instalados em seu Droplet e as permissões corretas estão definidas para o arquivo de configuração.

      Quando terminar, ou não precisar mais do seu Droplet, você poderá destruí-lo executando o comando kitchen destroy para excluí-lo após executar seus testes:

      Após este comando, você verá uma saída semelhante a:

      Output

      -----> Starting Kitchen (v1.24.0) -----> Destroying <default-ubuntu-18>... Finished destroying <default-ubuntu-18> (0m0.00s). -----> Kitchen is finished. (0m5.07s) 3.79s user 1.50s system 82% cpu 6.432 total

      Você escreveu testes para o seu playbook, executou os testes e corrigiu os testes com falha para garantir que todos os testes sejam aprovados. Agora você está pronto para criar um ambiente virtual, escrever testes para o seu Playbook Ansible e executar seu teste no ambiente virtual usando o Kitchen.

      Conclusão

      Agora você tem uma base flexível para testar seu deployment Ansible, que lhe permite testar seus playbooks antes de executar em um servidor ativo. Você também pode empacotar seu teste em um perfil. Você pode usar perfis para compartilhar seu teste através do Github ou do Chef Supermarket e executá-lo facilmente em um servidor ativo.

      Para detalhes mais abrangentes sobre o InSpec e o Kitchen, consulte a documentação oficial do InSpec e a documentação oficial do Kitchen.



      Source link