One place for hosting & domains

      módulo

      Como criar um servidor Web em Node.js com o módulo HTTP


      O autor selecionou a COVID-19 Relief Fund​​​​​ para receber uma doação como parte do programa Write for DOnations.

      Introdução

      Ao visualizar uma página em seu navegador, você está fazendo uma solicitação a outro computador na Internet, que em resposta, fornece a você a página Web. O computador ao qual você está se comunicando pela Internet é um servidor Web. Um servidor Web recebe solicitações HTTP de um cliente, como seu navegador, e fornece uma resposta HTTP, como uma página HTML ou um JSON de uma API.

      Para que um servidor retorne uma página da Web, vários softwares são envolvidos no processo. Normalmente, estes softwares se enquadram em duas categorias: o front-end e o back-end. O código de front-end lida com a forma como o conteúdo é apresentado, como a cor de uma barra de navegação e o estilo do texto. O código de back-end lida com a forma como os dados são trocados, processados e armazenados. O código que cuida das solicitações de rede do seu navegador, ou que se comunica com o banco de dados é gerenciado, principalmente, pelo código de back-end.

      O Node.js permite que os desenvolvedores utilizem o JavaScript para escrever o código de back-end, embora ele seja tradicionalmente usado no navegador para escrever o código de front-end. Ter o front-end e o back-end juntos reduz o esforço necessário para criar um servidor Web. Esse é o principal motivo pelo qual o Node.js é a escolha mais popular para escrever códigos de back-end.

      Neste tutorial, você aprenderá como desenvolver servidores Web usando o módulo http que está incluído no Node.js. Você desenvolverá servidores Web que podem retornar dados em JSON, arquivos CSV e páginas Web em HTML.

      Pré-requisitos

      • Verifique se o Node.js está instalado em sua máquina de desenvolvimento. Este tutorial utiliza a versão 10.19.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 intitulada Instalando usando um PPA, do artigo sobre Como instalar o Node.js no Ubuntu 18.04.
      • A plataforma Node.js pode ser usada para criar servidores Web prontos para uso. Antes de começar, você precisa estar familiarizado com os princípios básicos do Node.js. Para isso, revise nosso guia sobre Como escrever e executar seu primeiro programa em Node.js.
      • Também usamos a programação assíncrona em uma de nossas seções. Se você não estiver familiarizado com a programação assíncrona em Node.js ou com o uso do módulo fs para interagir com arquivos, você pode aprender mais sobre estes assuntos no nosso artigo Como escrever um código assíncrono em Node.js.

      Passo 1 — Criando um servidor HTTP básico

      Começaremos criando um servidor que retorna um texto sem formatação ao usuário. O processo abordará os conceitos fundamentais necessários para configurar um servidor, que fornecerão a base necessária para retornar os formatos de dados mais complexos, como o JSON.

      Primeiro, precisamos configurar um ambiente de programação acessível para fazer nossos exercícios, bem como os outros execícios no artigo. No terminal, crie uma pasta chamada first-servers:

      Então, acesse aquela pasta:

      Agora, crie o arquivo para guardar o código:

      Abra o arquivo em um editor de texto. Utilizaremos o nano, pois ele está disponível no terminal:

      Começaremos carregando o módulo http, que é padrão em todas as instalações do Node.js. Adicione a linha seguinte ao hello.js:

      first-servers/hello.js

      const http = require("http");
      

      O módulo http contém a função de criar o servidor, que veremos depois. Se quiser aprender mais a respeito de módulos em Node.js, consulte nosso artigo Como criar um módulo Node.js.

      Nosso próximo passo será definir duas constantes, o host e a porta em que nosso servidor se associará:

      first-servers/hello.js

      ...
      const host = 'localhost';
      const port = 8000;
      

      Como mencionado anteriormente, os servidores Web aceitam solicitações de navegadores e de outros clientes. Podemos interagir com um servidor Web ao digitar um nome de domínio, que é traduzido para um endereço IP por um servidor DNS. Um endereço IP é uma sequência única de números que identificam uma máquina em uma rede, como a Internet. Para obter mais informações sobre conceitos de nome de domínio, consulte nosso artigo de Introdução à terminologia, componentes e conceitos do DNS.

      O valor localhost é um endereço privado especial que os computadores utilizam para se referir a eles mesmos. Normalmente, ele é equivalente ao endereço IP interno 127.0.0.1 e está disponível apenas para o computador local, ou seja, não está disponível para nenhuma rede local da qual participamos ou para a Internet.

      A porta é um número que os servidores usam como um ponto de extremidade ou uma “passagem” para nosso endereço IP. Em nosso exemplo, utilizaremos a porta 8000 para nosso servidor Web. As portas 8080 e 8000 são normalmente usadas como as portas padrão em processos de desenvolvimento e, na maioria dos casos, os desenvolvedores utilizarão essas portas em vez de outras disponíveis para servidores HTTP.

      Ao vincularmos nosso servidor a este host e porta, conseguiremos acessar nosso servidor ao visitarmos http://localhost:8000 em um navegador local.

      Vamos adicionar uma função especial que em Node.js, chamamos de request listener. Esta função foi criada para processar uma solicitação HTTP de entrada e retornar uma resposta HTTP. A função deve ter dois argumentos: um objeto de solicitação e um objeto de resposta. O objeto de solicitação capta todos os dados da solicitação HTTP que chegam. O objeto de resposta é usado para devolver respostas HTTP para o servidor.

      Queremos que nosso primeiro servidor retorne a seguinte mensagem sempre que alguém o acessar: "My first server!".

      Vamos adicionar a função a seguir:

      first-servers/hello.js

      ...
      
      const requestListener = function (req, res) {
          res.writeHead(200);
          res.end("My first server!");
      };
      

      Normalmente, o nome de uma função baseia-se no que ela faz. Por exemplo, se criássemos uma função request listener para retornar uma lista de livros, daríamos a ela o nome listBooks(). Como este é um caso de exemplo, usaremos o nome genérico requestListener.

      Todas as funções request listener em Node,js aceitam dois argumentos: req e res (podemos dar nomes diferentes a eles se quisermos). A solicitação HTTP que o usuário envia é capturada em um objeto Request, que corresponde ao primeiro argumento, req. A resposta HTTP que retornamos para o usuário é formada pela interação com o objeto Response no segundo argumento, res.

      A primeira linha res.writeHead(200); define o código de status HTTP da resposta. Os códigos de status do HTTP indicam o quão bem uma solicitação HTTP foi processada pelo servidor. Neste caso, o código de status 200 corresponde a "OK". Se estiver interessado em aprender sobre os vários códigos HTTP que seus servidores Web podem retornar e o que eles significam, o nosso guia Como resolver problemas comuns de erros de códigos HTTP será um bom ponto de partida.

      A próxima linha da função res.end("My first server!") ; escreve a resposta HTTP e a retorna para o cliente que a solicitou. Esta função retorna todos os dados que o servidor precisa retornar. Neste caso, ele retorna dados de texto.

      Por fim, podemos criar nosso servidor e usar nosso request listener:

      first-servers/hello.js

      ...
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Salve e saia do nano pressionando CTRL+X.

      Na primeira linha, criamos um novo objeto server através da função createServer() do módulo http. Este servidor aceita solicitações HTTP e as passa para nossa função requestListener().

      Após criarmos nosso servidor, precisaremos associá-lo a um endereço de rede. Faremos isso com o método server.listen(). Ele aceita três argumentos: port, host e uma função de retorno de chamada que é acionada quando o servidor começa a escutar.

      Todos esses argumentos são opcionais, mas é aconselhável especificar qual porta e qual host queremos que o servidor Web utilize. Ao implantar servidores Web para diferentes ambientes, é importante saber a porta e o host nos quais ele está funcionando para configurar o balanceamento de carga ou um alias DNS.

      A função de retorno de chamada registra uma mensagem no nosso console para que possamos saber quando o servidor começou a escutar conexões.

      Nota: embora o requestListener() não utilize o objeto req, ele deve ser o primeiro argumento da função.

      Usando menos de quinze linhas de código, criamos um servidor Web. Vamos vê-lo em ação e testá-lo por completo, executando o programa:

      No console, veremos esta saída:

      Output

      Server is running on http://localhost:8000

      Note que o prompt desaparece. Isso acontece porque o servidor Node.js é um processo de longa duração. Ele só fecha caso encontre um erro que cause falha e encerramento, ou se interrompermos o processo do Node.js que está sendo executado o servidor.

      Em uma janela do terminal separada, nos comunicaremos com o servidor usando o cURL, uma ferramenta CLI que transfere dados para e a partir de uma rede. Digite o seguinte comando para fazer uma solicitação GET HTTP ao nosso servidor em execução:

      • curl http://localhost:8000

      Ao pressionar ENTER, nosso terminal mostrará o seguinte resultado:

      Output

      My first server!

      Acabamos de configurar um servidor e receber nossa primeira resposta do servidor.

      Vamos detalhar o que aconteceu quando testamos nosso servidor. Ao utilizar o cURL, enviamos uma solicitação GET para o servidor em http://localhost:8000. Nosso servidor Node.js escutou as conexões deste endereço. O servidor transmitiu a solicitação para a função requestListener(). A função retornou dados de texto com o código de status 200. Em seguida, o servidor enviou esta resposta de volta ao cURL, que exibiu a mensagem em nosso terminal.

      Antes de continuarmos, vamos sair de nosso servidor em execução pressionando CTRL+C. Isso interrompe a execução de nosso servidor, e nos traz de volta ao prompt de linha de comando.

      Na maioria dos sites que visitamos ou APIs que utilizamos, as respostas do servidor raramente são textos sem formatação. Os formatos de resposta mais comuns que recebemos são páginas HTML e dados JSON. No próximo passo, aprenderemos como retornar respostas HTTP em formatos de dados comuns que encontramos na Web.

      Passo 2 — Retornando tipos diferentes de conteúdo

      A resposta que retornamos a partir de um servidor Web pode ter vários formatos. O JSON e o HTML foram mencionados anteriormente e, além deles, podemos retornar outros formatos de texto, como o XML e o CSV. Por fim, os servidores Web podem retornar dados não textuais, como PDFs, arquivos zip, áudio e vídeo.

      Neste artigo, além do texto sem formatação que acabamos de retornar, você aprenderá como retornar os tipos de dados a seguir:

      Os três tipos de dados são baseados em texto e são formatos populares para o envio de conteúdos na Web. Muitas linguagens e ferramentas de desenvolvimento do servidor possuem recursos para retornar esses tipos diferentes de dados. No contexto do Node.js, precisaremos fazer duas coisas:

      1. Definir o cabeçalho do Content-Type em nossas respostas HTTP com o valor adequado.
      2. Confirmar que o res.end() recebe os dados no formato correto.

      Veremos isso em ação com alguns exemplos. O código que escreveremos nesta seção e em seções posteriores possui muitas semelhanças com o código que escrevemos anteriormente. A maioria das alterações existem dentro da função requestListener(). Criaremos arquivos com este “código modelo” para facilitar o acompanhamento das próximas seções.

      Crie um arquivo novo chamado html.js. Este arquivo será usado mais tarde para retornar texto HTML em uma resposta HTTP. Colocaremos o código modelo aqui e o copiaremos para os outros servidores que retornam vários tipos.

      No terminal, digite o seguinte:

      Abra este arquivo em um editor de texto:

      Copie o “código modelo” Cole o código no nano:

      first-servers/html.js

      const http = require("http");
      
      const host = 'localhost';
      const port = 8000;
      
      const requestListener = function (req, res) {};
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Salve e saia do html.js com CTRL+X e, em seguida, retorne ao terminal.

      Vamos copiar este arquivo em dois arquivos novos. O primeiro arquivo terá o objetivo de retornar dados CSV na resposta HTTP:

      O segundo arquivo retornará uma resposta JSON no servidor:

      Os arquivos restantes serão úteis para exercícios posteriores:

      • cp html.js htmlFile.js
      • cp html.js routes.js

      Agora estamos prontos para continuar nossos exercícios. Começaremos retornando o JSON.

      Apresentando o JSON

      Mais conhecido como JSON, o JavaScript Object Notation é um formato de troca de dados baseado em texto. Como o nome sugere, ele é derivado de objetos do JavaScript, mas é independente de linguagem e pode ser usado por qualquer linguagem de programação que possa analisar sua sintaxe.

      O JSON é geralmente usado pelas APIs para aceitar e retornar dados. Sua popularidade se deve ao fato de ser um formato de transferência de dados mais leve que os padrões anteriores a ele, como o XML, bem como às ferramentas que permitem que esse dados sejam analisados pelos programas sem muito esforço. Se quiser aprender mais sobre o JSON, leia nosso guia sobre Como trabalhar com JSON em JavaScript.

      Abra o arquivo json.js com o nano:

      Queremos retornar uma resposta JSON. Vamos modificar a função requestListener() para retornar o cabeçalho apropriado a respostas JSON, modificando as linhas destacadas da seguinte maneira:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      };
      ...
      

      O método res.setHeader() adiciona um cabeçalho HTTP à resposta. Os cabeçalhos HTTP são informações adicionais que podem ser anexadas a uma solicitação ou uma resposta. O método res.setHeader() recebe dois argumentos: o nome do cabeçalho e o valor dele.

      O cabeçalho Content-Type é usado para indicar o formato dos dados, também conhecido como tipo de mídia, que está sendo enviado com a solicitação ou resposta. Neste caso, nosso Content-Type é o application/json.

      Vamos retornar o conteúdo JSON ao usuário. Modifique o json.js para que ele se pareça com isto:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          res.writeHead(200);
          res.end(`{"message": "This is a JSON response"}`);
      };
      ...
      

      Assim como antes, diremos aos usuários que as solicitações deles foram bem-sucedidas, retornando um código de status 200. Desta vez, na chamada response.end(), nosso argumento string contém um JSON válido.

      Salve e saia do json.js pressionando CTRL+X. Agora, vamos executar o servidor com o comando node:

      Em outro terminal, vamos acessar o servidor usando o cURL:

      • curl http://localhost:8000

      Ao pressionar ENTER, veremos o seguinte resultado:

      Output

      {"message": "This is a JSON response"}

      Agora, retornamos com sucesso uma resposta JSON, à semelhança de várias APIs populares que usamos para criar aplicativos. Você precisa sair do servidor em execução, usando CTRL+C, para que possamos retornar ao prompt do terminal padrão. Em seguida, vamos examinar outro formato popular de retorno de dados: o CSV.

      Apresentando o CSV

      O formato de arquivos CSV (valores separados por vírgula) é um padrão de texto muito usado para fornecer dados tabulares. Na maioria dos casos, cada linha é separada por um caractere de nova linha, e cada item da linha é separado por uma vírgula.

      Em nosso espaço de trabalho, abra o arquivo csv.js com um editor de texto:

      Vamos adicionar as linhas seguintes para nossa função requestListener():

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
      };
      ...
      

      Desta vez, nosso Content-Type indica que um arquivo CSV está sendo retornado, pois o valor é text/csv. O segundo cabeçalho que adicionamos é o Content-Disposition. Este cabeçalho diz ao navegador como exibir os dados, em particular no navegador, ou como um arquivo separado.

      Ao retornarmos respostas CSV, a maioria dos navegadores modernos baixam automaticamente o arquivo, mesmo se o cabeçalho Content-Disposition não estiver definido. No entanto, ao retornar um arquivo CSV, devemos adicionar este cabeçalho mesmo assim, pois ele nos permite definir o nome do arquivo CSV. Neste caso, sinalizamos ao navegador que este arquivo CSV é um anexo e ele deve ser baixado. Em seguida, dizemos ao navegador que o nome do arquivo é oceanpals.csv.

      Vamos escrever os dados CSV na resposta HTTP:

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
          res.writeHead(200);
          res.end(`id,name,emailn1,Sammy Shark,shark@ocean.com`);
      };
      ...
      

      Assim como antes, retornarmos um status 200/OK com nossa resposta. Desta vez, nossa chamada para o res.end() tem uma string que é uma CSV válida. A vírgula separa o valor de cada coluna e o caractere de nova linha (n) separa as linhas. Temos duas linhas, uma para o cabeçalho da tabela e outra para os dados.

      Testaremos este servidor no navegador. Salve o csv.js e saia do editor com o CTRL+X.

      Execute o servidor com o comando Node.js:

      Em outro terminal, acesse o servidor pelo cURL:

      • curl http://localhost:8000

      O console mostrará isso:

      Output

      id,name,email 1,Sammy Shark,shark@ocean.com

      Se formos para http://localhost:8000 em nosso navegador, um arquivo CSV será baixado. O nome do arquivo será oceanpals.csv.

      Saia do servidor em execução com CTRL+C, para retornar ao prompt do terminal padrão.

      Ao retornar o JSON e o CSV, abordamos dois casos populares das APIs. Em seguida, vamos abordar como retornar dados para sites que as pessoas visualizam em um navegador.

      Apresentando o HTML

      O HTML, ou HyperText Markup Language, é o formato mais comum utilizado quando queremos que usuários interajam com nosso servidor através de um navegador Web. Ele foi criado para estruturar o conteúdo Web. Os navegadores Web são desenvolvidos para exibir conteúdo HTML, bem como qualquer estilo que adicionarmos com o CSS (outra tecnologia Web de front-end que nos permite alterar a estética de nossos sites).

      Vamos abrir novamente o html.js com nosso editor de texto:

      Modifique a função requestListener() para retornar o cabeçalho de Content-Type apropriado para uma resposta HTML:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
      };
      ...
      

      Vamos retornar o conteúdo HTML ao usuário. Adicione as linhas destacadas ao html.js para que se pareça com isso:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(`<html><body><h1>This is HTML</h1></body></html>`);
      };
      ...
      

      Adicionamos primeiro o código de status HTTP. Em seguida, chamamos o response.end() com um argumento string que contém um HTML válido. Quando acessarmos nosso servidor no navegador, veremos uma página HTML com uma tag de cabeçalho que contém This is HTML.

      Vamos salvar e sair pressionando o CTRL+X. Agora, vamos executar o servidor com o comando node:

      Veremos a mensagem Server is running on http://localhost:8000 quando nosso programa for iniciado.

      Vá para o navegador e visite http://localhost:8000. Nossa página se parecerá com esta:

      Imagem da resposta HTML retornada do servidor Node.js

      Vamos encerrar a execução do servidor com CTRL+C e retornar ao prompt do terminal padrão.

      É comum que o HTML seja escrito em um arquivo separado do código do servidor, como nossos programas do Node.js. Em seguida, veremos como retornar respostas HTML a partir dos arquivos.

      Passo 3 — Apresentando uma página HTML a partir de um arquivo

      Podemos apresentar aos usuários o HTML como strings no Node.js, mas é melhor carregar os arquivos HTML e exibir seu conteúdo a eles. Desta maneira, não precisamos manter strings longas em nosso código Node.js à medida que o arquivo HTML cresce. Isso mantém o código mais conciso e permite que trabalhemos em cada aspecto do nosso site de maneira independente. Esta “separação de problemas” é comum em várias configurações de desenvolvimento Web. Assim, é bom saber como carregar arquivos HTML para permitir esta separação no Node.js.

      Para exibir arquivos HTML aplicaremos o módulo fs para carregar os arquivos e usaremos os dados dele ao escrever nossa resposta HTTP.

      Primeiro, criaremos um arquivo HTML, que será retornado pelo servidor Web quando solicitado. Crie um novo arquivo HTML:

      Abra o index.html em um editor de texto:

      Nossa página Web será bem simples. Ela terá um plano de fundo e exibirá um texto de saudação no centro. Adicione este código ao arquivo:

      first-servers/index.html

      <!DOCTYPE html>
      
      <head>
          <title>My Website</title>
          <style>
              *,
              html {
                  margin: 0;
                  padding: 0;
                  border: 0;
              }
      
              html {
                  width: 100%;
                  height: 100%;
              }
      
              body {
                  width: 100%;
                  height: 100%;
                  position: relative;
                  background-color: rgb(236, 152, 42);
              }
      
              .center {
                  width: 100%;
                  height: 50%;
                  margin: 0;
                  position: absolute;
                  top: 50%;
                  left: 50%;
                  transform: translate(-50%, -50%);
                  color: white;
                  font-family: "Trebuchet MS", Helvetica, sans-serif;
                  text-align: center;
              }
      
              h1 {
                  font-size: 144px;
              }
      
              p {
                  font-size: 64px;
              }
          </style>
      </head>
      
      <body>
          <div class="center">
              <h1>Hello Again!</h1>
              <p>This is served from a file</p>
          </div>
      </body>
      
      </html>
      

      Esta página Web mostra duas linhas de texto: Hello Again! e This is served from a file. As linhas aparecem no centro da página, uma acima da outra. A primeira linha de texto é exibida como um título, ou seja, em letras maiores. A segunda linha de texto aparecerá em letras ligeiramente menores. Todo o texto aparecerá na cor branca enquanto a página Web tem plano de fundo laranja.

      Embora não seja o âmbito deste artigo ou série, se estiver interessado em aprender mais sobre HTML, CSS e outras tecnologias Web de front-end, dê uma olhada no guia Introdução à Web do Mozilla.

      Isso é tudo o que precisamos para o HTML. Assim, salve e saia do arquivo com o CTRL+X. Agora, podemos avançar para o código do servidor.

      Para este exercício, trabalharemos com o htmlFile.js. Abra-o com o editor de texto:

      Como temos que ler um arquivo, vamos começar importando o módulo fs:

      first-servers/htmlFile.js

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

      Este módulo contém uma função readFile() que utilizaremos para carregar o arquivo HTML corretamente. Importamos o objeto promessa do tipo variant, de acordo com as práticas modernas recomendadas para o JavaScript. Usamos as promessas por serem sintaticamente mais sucintas em comparação ao retorno de chamada, que teríamos que usar se atribuíssemos o fs para fazer somente o require('fs'). Para aprender mais sobre as práticas recomendadas da programação assíncrona, leia nosso guia Como escrever um código assíncrono no Node.js.

      Queremos que nosso arquivo HTML seja lido quando um usuário solicitar nosso sistema. Começaremos modificando o requestListener() para ler o arquivo:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
      };
      ...
      

      Usamos o método fs.readFile() para carregar o arquivo. Seu argumento possui o __dirname + "/index.html". A variável especial __dirname tem o caminho absoluto de onde o código Node.js está sendo executado. Em seguida, acrescentamos o /index.html para poder carregar o arquivo HTML que criamos mais cedo.

      Agora, vamos retornar a página HTML assim que estiver carregada:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
      };
      ...
      

      Se a promessa fs.readFile() for resolvida com sucesso, ela retornará os dados dela. Usamos o método then() para processar este caso. O parâmetro contents contém os dados do arquivo HTML.

      Primeiramente, definimos o cabeçalho Content-Type para text/html para dizer ao cliente que estamos retornando dados HTML. Em seguida, escrevemos o código do status para indicar que a solicitação foi bem-sucedida. Por fim, enviamos ao cliente a página HTML que carregamos, com os dados na variável contents.

      O método fs.readFile() pode falhar ocasionalmente, por este motivo, precisamos saber como lidar com esta questão quando recebermos um erro. Adicione isso à função requestListener():

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
              .catch(err => {
                  res.writeHead(500);
                  res.end(err);
                  return;
              });
      };
      ...
      

      Salve o arquivo e saia do nano com o CTRL+X.

      Quando uma promessa encontra um erro, ela é rejeitada. Trataremos esta questão usando o método catch(). Ele aceita o erro que o fs.readFile() retorna, define o código de status para 500 sinalizando que um erro interno foi encontrado, e retorna o erro para o usuário.

      Execute nosso servidor com o comando node:

      No navegador Web, visite http://localhost:8000​​​. Você verá esta página:

      Imagem da página HTML carregada a partir de um arquivo no Node.js

      Você retornou uma página HTML a partir de um servidor para o usuário. Você pode encerrar a execução do servidor com o CTRL+C. Você verá o prompt do terminal quando encerrar o servidor.

      Ao escrever um código como este em um ambiente de produção, não é recomendável carregar uma página HTML toda vez que você receber uma solicitação HTTP. Embora essa página HTML tenha cerca de 800 bytes em tamanho, sites mais complexos podem ter megabytes em tamanho. Arquivos muito grandes podem levar um bom tempo para carregar. Se for esperado que seu site tenha um tráfego intenso, é recomendável carregar os arquivos HTML no momento da inicialização, além de salvar o conteúdo deles. Após eles carregarem, defina o servidor e faça-o escutar solicitações em um endereço.

      Para demonstrar este método, veremos como podemos retrabalhar nosso servidor para torná-lo mais eficiente e escalonável.

      Apresentando o HTML adequadamente

      Neste passo, em vez de carregar o HTML para cada solicitação, o carregaremos apenas uma vez, no início. A solicitação retornará os dados que carregamos na inicialização.

      No terminal, abra novamente o script do Node.js com um editor de texto:

      Começaremos adicionando uma nova variável antes de criarmos a função requestListener():

      first-servers/htmlFile.js

      ...
      let indexFile;
      
      const requestListener = function (req, res) {
      ...
      

      Quando executarmos este programa, esta variável reterá o conteúdo do arquivo HTML.

      Agora, vamos reajustar a função requestListener(). Em vez de carregar o arquivo, ele retornará o conteúdo do indexFile:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(indexFile);
      };
      ...
      

      Em seguida, trocaremos a lógica de leitura do arquivo, da função requestListener() para a inicialização do nosso servidor. Faça as seguintes alterações à medida que criamos o servidor:

      first-servers/htmlFile.js

      ...
      
      const server = http.createServer(requestListener);
      
      fs.readFile(__dirname + "/index.html")
          .then(contents => {
              indexFile = contents;
              server.listen(port, host, () => {
                  console.log(`Server is running on http://${host}:${port}`);
              });
          })
          .catch(err => {
              console.error(`Could not read index.html file: ${err}`);
              process.exit(1);
          });
      

      Salve o arquivo e saia do nano com o CTRL+X.

      O código que lê o arquivo é parecido com o que escrevemos em nossa primeira tentativa. No entanto, quando a leitura do arquivo for bem-sucedida, salvaremos seu conteúdo em nossa variável global indexFile. Em seguida, iniciamos o servidor com o método listen(). O ponto principal é que o arquivo precisa ser carregado antes do servidor ser executado. Desta maneira, a função requestListener() certamente retornará uma página HTML, pois a variável indexFile não estará vazia.

      Nosso manipulador de erros também foi alterado. Se o arquivo não puder ser carregado, o erro será capturado e exibido em nosso console. Em seguida, saímos do programa Node.js com a função exit() sem iniciar o servidor. Desta maneira, podemos ver a razão pela qual a leitura do arquivo falhou, resolver o problema e, em seguida, iniciar novamente o servidor.

      Nós criamos servidores Web diferentes que retornam vários tipos de dados para um usuário. Até agora, não utilizamos nenhuma solicitação de dados para determinar o que deveria ser retornado. Precisaremos usar dados de solicitação ao configurar rotas ou caminhos diferentes em um servidor Node.js. Por este motivo, veremos a seguir como eles funcionam juntos.

      Passo 4 — Gerenciando rotas usando um objeto de solicitação HTTP

      A maioria dos sites que visitamos ou APIs que utilizamos geralmente possui mais de um ponto de extremidade, para que possamos acessar vários recursos. Um bom exemplo disso seria um sistema de gerenciamento de livros que poderia ser usado em uma biblioteca. Esse sistema precisaria gerenciar não apenas os dados de livros, mas também os dados de autores, para facilitar os processos de catalogação e consulta.

      Embora os dados para livros e autores estejam relacionados, eles são dois objetos diferentes. Nestes casos, os desenvolvedores de software normalmente programam pontos de extremidades diferentes, como uma maneira de indicar aos usuários da API com quais tipos de dados eles estão interagindo.

      Vamos criar um novo servidor para uma biblioteca pequena, com o propósito de retornar dois tipos diferentes de dados. Se um usuário acessar o endereço de nosso servidor em /books, ele receberá uma lista de livros em JSON. Se eles forem para /authors, receberão uma lista de informações do autor em JSON.

      O que fizemos até agora foi retornar a mesma resposta para cada solicitação que recebemos. Vamos ilustrar isso rapidamente.

      Execute novamente nosso exemplo de resposta JSON:

      Em outro terminal, faremos a mesma solicitação cURL de antes:

      • curl http://localhost:8000

      Você verá:

      Output

      {"message": "This is a JSON response"}

      Agora, vamos testar outro comando curl:

      • curl http://localhost:8000/todos

      Após pressionar Enter, verá o mesmo resultado:

      Output

      {"message": "This is a JSON response"}

      Nós não desenvolvemos nenhuma lógica especial em nossa função requestListener() que possa processar um pedido cuja URL contenha /todos. Por esse motivo, o Node.js retorna a mesma mensagem JSON por padrão.

      Como queremos desenvolver um servidor de gerenciamento para uma biblioteca pequena, separaremos os tipos de dados retornados de acordo com o ponto de extremidade que o usuário acessar.

      Primeiro, saia do servidor em execução com o CTRL+C.

      Abra o routes.js em seu editor de texto:

      Começaremos armazenando nossos dados JSON em variáveis antes da função requestListener():

      first-servers/routes.js

      ...
      const books = JSON.stringify([
          { title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
          { title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
      ]);
      
      const authors = JSON.stringify([
          { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
          { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
      ]);
      ...
      

      A variável books é uma string que contém dados JSON para uma matriz de objetos do tipo livro. Cada livro tem um título ou nome, um autor e o ano de publicação.

      A variável authors é uma string que contém o JSON para uma matriz de objetos do tipo autor. Cada autor tem um nome, país de origem e seu ano de nascimento.

      Agora que temos os dados que nossas respostas retornarão, vamos começar a modificar a função requestListener() para retornar as rotas corretas.

      Primeiro, vamos garantir que cada resposta de nosso servidor tenha o cabeçalho Content-Type correto:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      }
      ...
      

      Agora, queremos retornar o JSON correto de acordo com o caminho da URL que o usuário acessar. Vamos criar uma instrução de switch na URL da solicitação:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {}
      }
      ...
      

      Para obter o caminho da URL de um objeto de solicitação, precisamos acessar sua propriedade url. Podemos adicionar casos à instrução switch para retornar o JSON apropriado.

      A instrução switch do JavaScript fornece uma maneira de controlar qual código é executado, dependendo do valor de um objeto ou expressão JavaScript (por exemplo, o resultado de operações matemáticas). Se precisar aprender ou recordar como utilizá-las, consulte nosso guia sobre Como usar a instrução switch em JavaScript.

      Vamos seguir em frente adicionando um case para quando o usuário quiser receber nossa lista de livros:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
          }
      }
      ...
      

      Definimos nosso código de status para 200, que indica que está tudo bem com a solicitação, e retorna o JSON que contém a lista de nossos livros. Agora, vamos adicionar outro case para nossos autores:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
          }
      }
      ...
      

      Assim como antes, o código de status será 200, pois não há nada errado com a solicitação. Desta vez retornamos o JSON que contém a lista de autores.

      Temos que retornar um erro caso o usuário tente acessar qualquer outro caminho. Para fazermos isso, adicionaremos o caso padrão:

      routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
              default:
                  res.writeHead(404);
                  res.end(JSON.stringify({error:"Resource not found"}));
          }
      }
      ...
      

      Usamos a palavra-chave default em uma instrução switch para capturar todos os outros cenários que não foram capturados pelos nossos casos anteriores. Definimos o código de status para 404, que indica que a URL que eles estavam procurando não foi encontrada. Em seguida, definimos um objeto JSON que contém uma mensagem de erro.

      Vamos testar nosso servidor para verificar se ele se comporta como o esperado. Em outro terminal, executamos primeiro um comando para ver se recebemos nossa lista de livros:

      • curl http://localhost:8000/books

      Pressione Enter para ver o seguinte resultado:

      Output

      [{"title":"The Alchemist","author":"Paulo Coelho","year":1988},{"title":"The Prophet","author":"Kahlil Gibran","year":1923}]

      Até agora, tudo certo. Vamos fazer o mesmo processo para o /authors. Digite o comando a seguir no terminal:

      • curl http://localhost:8000/authors

      Você verá o seguinte resultado quando o comando for concluído:

      Output

      [{"name":"Paulo Coelho","countryOfBirth":"Brazil","yearOfBirth":1947},{"name":"Kahlil Gibran","countryOfBirth":"Lebanon","yearOfBirth":1883}]

      Vamos testar uma URL errada para confirmar que o requestListener() retorna a resposta de erro:

      • curl http://localhost:8000/notreal

      Ao digitar este comando, a seguinte mensagem será exibida:

      Output

      {"error":"Resource not found"}

      Você pode sair da execução do servidor com o CTRL+C.

      Acabamos de criar caminhos diferentes para que os usuários obtenham dados diferentes. Nós também adicionamos uma resposta padrão que retornará um erro HTTP caso o usuário digite uma URL não compatível.

      Conclusão

      Neste tutorial, você criou uma série de servidores HTTP Node.js. Primeiro, você retornou uma resposta de texto básica. Em seguida, avançou para retornar vários tipos de dados a partir de nosso servidor: JSON, CSV e HTML. A partir de então, você conseguiu combinar o carregamento de arquivos com as respostas HTTP para retornar uma página HTML do servidor para o usuário, além de criar uma API que usou informações da solicitação do usuário para determinar quais dados deveriam ser enviados em resposta a esta solicitação.

      Você está preparado para criar servidores Web que podem processar uma variedade de solicitações e respostas. Com esse conhecimento, é possível criar um servidor que retorna várias páginas HTML para usuários em pontos de extremidades diferentes. É possível também criar sua própria API.

      Para aprender mais sobre servidores Web HTTP em Node.js, leia a documentação do Node.js no módulo http. Caso queira continuar aprendendo sobre o Node.js, volte para a página da série Como programar em Node.js.



      Source link

      Cómo crear un servidor web en Node.js con el módulo HTTP


      El autor seleccionó el COVID-19 Relief Fund para que reciba una donación como parte del programa Write for DOnations.

      Introducción

      Cuando visualiza una página web en su navegador, está realizando una solicitud a otro equipo en Internet, que a continuación proporciona la página web como respuesta. Esa computadora con la que está hablando a través de Internet es un servidor web. Un servidor web recibe solicitudes HTTP de un cliente, como su navegador, y proporciona una respuesta HTTP, como una página HTML o JSON desde una API.

      Para que un servidor devuelva una página web, se emplea una gran cantidad de software. Este software generalmente se divide en dos categorías: frontend y backend. El código front-end se refiere a cómo se presenta el contenido, como el color de la barra de navegación y el estilo de texto. El código back-end se encarga de la forma en la que los datos se intercambian, procesan y almacenan. El código que administra las solicitudes de red desde su navegador o se comunica con la base de datos lo gestiona principalmente el código back-end.

      Node.js permite a los desarrolladores usar JavaScript para escribir código back-end, aunque tradicionalmente se usaba en el navegador para escribir código front-end. Al tener el frontend y el backend juntos de esta forma, se reduce el esfuerzo necesario para crear un servidor web, que es el motivo principal por el que Node.js es una opción popular para escribir código back-end.

      En este tutorial, aprenderá cómo construir servidores web usando el módulo http que se incluye en Node.js. Creará servidores web que pueden devolver datos JSON, archivos CSV y páginas web HTML.

      Requisitos previos

      Paso 1: Crear un servidor HTTP básico

      Comenzaremos creando un servidor que devuelve texto sin formato al usuario. Esto cubrirá los conceptos clave necesarios para configurar un servidor, que proporcionará la base necesaria para devolver formatos de datos más complejos como JSON.

      Primero, debemos configurar un entorno de codificación accesible para hacer nuestros ejercicios y los demás en este artículo. En el terminal, cree una carpeta llamada first-servers:

      Luego, acceda a esa carpeta:

      Ahora, cree el archivo en donde se alojará el código:

      Abra el archivo en un editor de texto. Usaremos nano, ya que está disponible en el terminal:

      Comenzaremos cargando el módulo http que es estándar con todas las instalaciones de Node.js. Añada la siguiente línea a hello.js:

      first-servers/hello.js

      const http = require("http");
      

      El módulo http contiene la función para crear el servidor, que veremos más adelante. Si desea obtener más información sobre los módulos en Node.js, consulte nuestro artículo Cómo crear un módulo Node.js.

      Nuestro siguiente paso será definir dos constantes, el host y el puerto a los que se vinculará nuestro servidor:

      first-servers/hello.js

      ...
      const host = 'localhost';
      const port = 8000;
      

      Como mencionamos antes, los servidores web aceptan solicitudes de los navegadores y de otros clientes. Podemos interactuar con un servidor web ingresando un nombre de dominio, que se traduce a una dirección IP mediante un servidor DNS. Una dirección IP es una secuencia única de números que identifica un equipo en una red, como Internet. Para obtener más información sobre los conceptos de nombre de dominio, eche un vistazo a nuestro artículo Una introducción a la terminología DNS, componentes y conceptos.

      El valor localhost es una dirección privada especial que los ordenadores usan para referirse a ellos mismos. Normalmente, es el equivalente a la dirección IP interna 127.0.0.1 y solo está disponible para el equipo local, no para cualquier otra red a la que nos unamos o a Internet.

      El puerto es un número que los servidores usan como endpoint o “puerta” a nuestra dirección IP. En nuestro ejemplo, usaremos el puerto 8000 para nuestro servidor web. Los puertos 8080 y 8000 se usan normalmente como puertos predeterminados en desarrollo, y, en la mayoría de los casos, los desarrolladores los usarán en vez de los otros puertos para los servidores HTTP.

      Cuando vinculamos nuestro servidor a este host y puerto, podremos conectarnos a nuestro servidor visitando http://localhost:8000 en un navegador local.

      Ahora, añadiremos una función especial, que en Node.js llamamos una escucha de solicitudes. Esta función está destinada a gestionar una solicitud HTTP entrante y devolver una respuesta HTTP. Esta función debe tener dos argumentos, un objeto de solicitud y un objeto de respuesta. El objeto de solicitud captura todos los datos de la solicitud HTTP que está entrando. El objeto de respuesta se usa para devolver respuestas HTTP para el servidor.

      Queremos que nuestro primer servidor devuelva este mensaje siempre que alguien acceda a él: "My first server!".

      Ahora agreguemos esa función:

      first-servers/hello.js

      ...
      
      const requestListener = function (req, res) {
          res.writeHead(200);
          res.end("My first server!");
      };
      

      La función normalmente recibe su nombre según lo que hace. Por ejemplo, si creamos una función de escucha de solicitudes para devolver una lista de libros, probablemente la llamaremos listBooks(). Ya que este es un ejemplo, usaremos el nombre genérico requestListener.

      Todas las funciones de escucha de solicitudes en Node.js aceptan dos argumentos: req y res (podemos llamarlos de forma diferente si lo deseamos). La solicitud HTTP que el usuario envía se captura en un objeto Request, que se corresponde con el primer argumento, req. La respuesta HTTP que devolvemos al usuario se forma interactuando con el objeto Response en el segundo argumento, res.

      La primera línea res.writeHead(200); establece el código de estado HTTP de la respuesta. El código de estado HTTP indica si la solicitud HTTP fue gestionada correctamente por el servidor. En este caso, el código de estado 200 se corresponde con "OK". Si está interesado en aprender sobre los diferentes códigos HTTP que sus servidores web pueden devolver con el significado que tienen, nuestra guía Cómo resolver códigos de error HTTP comunes es un buen comienzo.

      La siguiente línea de la función, res.end("My first server"!) ; escribe la respuesta HTTP de vuelta al cliente que la solicitó. Esta función devuelve cualquier dato que el servidor tenga para devolver. En este caso, está devolviendo datos de texto.

      Finalmente, ahora podemos crear nuestro servidor y usar nuestra escucha de solicitudes.

      first-servers/hello.js

      ...
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Guarde y salga de nano pulsando CTRL+X.

      En la primera línea, creamos un nuevo objeto server a través de la función createServer() del módulo http. Este servidor acepta solicitudes HTTP y las pasa a nuestra función requestListener().

      Tras crear nuestro servidor, debemos vincularlo a una dirección de red. Hacemos eso con el método server.listen(). Acepta tres argumentos: puerto, host y una función de llamada que se activa cuando el servidor empieza a escuchar.

      Todos estos argumentos son opcionales, pero es una buena idea indicar explícitamente qué puerto y host queremos que use el servidor web. Cuando se implementan servidores web en entornos diferentes, conocer el puerto y el host en los que se está ejecutando es necesario para establecer el equilibrio de carga o un alias de DNS.

      La función callback registra un mensaje en nuestra consola para que sepamos cuándo el servidor comenzó a escuchar conexiones.

      Nota: Aunque requestListener() no usa el objeto req, debe seguir siendo el primer argumento de la función.

      Con menos de quince líneas de código, ahora tenemos un servidor web. Veámoslo en acción y hagamos una prueba de extremo a extremo ejecutando el programa:

      En la consola, veremos este resultado:

      Output

      Server is running on http://localhost:8000

      Observe que la solicitud desaparece. Esto es porque un servidor Node.js es un proceso largo de ejecución. Solo existe si se encuentra un error que haga que se detenga y se cierre, o si detenemos el proceso de Node.js ejecutando el servidor.

      En una ventana de terminal independiente, nos comunicaremos con el servidor usando cURL, una herramienta CLI para transferir datos hacia y desde una red. Ingrese el comando para realizar una solicitud GET HTTP a nuestro servidor en ejecución:

      • curl http://localhost:8000

      Cuando pulsamos ENTER, nuestro terminal mostrará el siguiente resultado:

      Output

      My first server!

      Ahora hemos configurado y servidor, y tenemos nuestra primera respuesta del servidor.

      Desglosemos lo que sucedió cuando probamos nuestro servidor. Usando cURL, enviamos una solicitud GET al servidor en http://localhost:8000. Nuestro servidor Node.js escuchó las conexiones de esa dirección. El servidor pasó esa solicitud a la función requestListener(). La función devolvió datos de texto con el código de estado 200. El servidor, luego, envió esa respuesta de vuelta a cURL, que mostró el mensaje en nuestro terminal.

      Antes de continuar, vamos a salir de nuestro servidor en ejecución pulsando CTRL+C. Esto interrumpe la ejecución de nuestro servidor, lo que nos lleva de regreso a la línea de comandos.

      En la mayoría de los sitios web que visitamos o las APIs que usamos, las respuestas del servidor son raramente texto sin formato. Obtenemos páginas HTML y datos JSON como formatos de respuesta comunes. En el siguiente paso, aprenderá cómo devolver respuestas HTTP en formatos de datos comunes que encontramos en la web.

      Paso 2: Devolver tipos de contenido diferentes

      La respuesta que devolvemos de un servidor web puede tener varios formatos. Hemos mencionado JSON y HTML, y podemos devolver otros formatos de texto como XML y CSV. Finalmente, los servidores web pueden devolver datos que no son texto, como PDFs, archivos comprimidos, audio y vídeo.

      En este artículo, además del texto sin formato que acabamos de devolver, aprenderá cómo devolver los siguientes tipos de datos:

      Estos tres tipos de datos están basados en texto y son formatos populares para entregar contenido en la web. Muchos lenguajes y herramientas de desarrollo del lado del servidor pueden devolver estos diferentes tipos de datos. En el contexto de Node.js, necesitamos hacer dos cosas:

      1. establecer el encabezado Content-Type en nuestras respuestas HTTP con el valor apropiado.
      2. asegurarnos de que res.end() obtiene los datos en el formato adecuado.

      Veamos esto en acción con algunos ejemplos. El código que escribiremos en esta sección y en las posteriores tienen muchas similitudes con el código que escribimos previamente. La mayoría de los cambios existen en la función requestListener(). Vamos a crear archivos con este “código de plantilla” para hacer que las siguientes secciones sean más fáciles de seguir.

      Cree un nuevo archivo llamado html.js. Este archivo se usará más adelante para devolver texto HTML en una respuesta HTTP. Pondremos el código de la plantilla aquí y lo copiaremos a los otros servidores que devuelven varios tipos.

      En el terminal, ingrese lo siguiente:

      Ahora abra este archivo en un editor de texto:

      Vamos a copiar el “código de la plantilla”. Ingrese esto en nano:

      first-servers/html.js

      const http = require("http");
      
      const host = 'localhost';
      const port = 8000;
      
      const requestListener = function (req, res) {};
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Guarde y salga de html.js con CTRL+X, luego vuelva al terminal.

      Ahora, vamos a copiar este archivo en dos nuevos archivos. El primero archivo será para devolver datos CSV en la respuesta HTTP:

      El segundo archivo devolverá una respuesta JSON en el servidor:

      Los archivos restantes serán para ejercicios posteriores:

      • cp html.js htmlFile.js
      • cp html.js routes.js

      Ahora estamos listos para continuar con nuestros ejercicios. Vamos a comenzar con devolver JSON.

      Presentar JSON

      JavaScript Object Notation, normalmente conocido como JSON, es un formato de intercambio de datos basado en texto. Como su nombre lo indica, se deriva de los objetos JavaScript, pero es un lenguaje independiente, lo que significa que puede ser usado por cualquier lenguaje de programación que pueda analizar su sintaxis.

      Las APIS comúnmente usan JSON para aceptar y devolver datos. Su popularidad se debe al menor tamaño de transferencia de datos que los estándares de intercambio de datos como XML, además de las herramientas que existen para permitir que los programas las analicen sin esfuerzo excesivo. Si desea obtener más información sobre JSON, puede leer nuestra guía sobre Cómo trabajar con JSON en JavaScript.

      Abra el archivo json.js con nano:

      Queremos devolver una respuesta JSON. Ahora, modifiquemos la función requestListener() para devolver al encabezado apropiado todas las respuestas que JSON tiene cambiando las líneas resaltadas de esta forma:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      };
      ...
      

      El método res.setHeader() añade un encabezado HTTP a la respuesta. Los encabezados HTTP son información adicional que puede adjuntarse a una solicitud o respuesta. El método res.setHeader() toma dos argumentos: el nombre del encabezado y su valor.

      El encabezado Content-Type se usa para indicar el formato de los datos, también conocido como tipo de medio, que se envían con la solicitud o respuesta. En este caso, nuestro Content-Type es application/json.

      Ahora vamos a devolver contenido JSON al usuario. Modifique json.js para que tenga este aspecto:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          res.writeHead(200);
          res.end(`{"message": "This is a JSON response"}`);
      };
      ...
      

      Como antes, indicamos al usuario que su solicitud se realizó correctamente devolviendo un código de estado de 200. Esta vez, en la invocación response.end(), nuestro argumento de cadena contiene JSON válido.

      Guarde y salga de jason.js pulsando CTRL+X. Ahora, ejecutaremos el servidor con el comando node:

      En otro terminal, vamos a contactar con el servidor usando cURL:

      • curl http://localhost:8000

      Cuando pulsemos ENTER, veremos el siguiente resultado:

      Output

      {"message": "This is a JSON response"}

      Ahora tenemos una respuesta JSON devuelva, como muchas de las APIs populares con las que creamos aplicaciones. Asegúrese de salir del servidor en ejecución con CTRL+C para poder volver a la solicitud estándar del terminal. Veamos otro formato popular para devolver datos: CSV.

      Presentar CSV

      El formato de archivo Valores separados por coma (CSV) es un estándar de texto que se usa comúnmente para proporcionar datos tabulares. En la mayoría de los casos, cada fila se separa por una nueva línea, y cada elemento de la fila se separa por una coma.

      En nuestro espacio de trabajo, abra el archivo csv.js en un editor de texto:

      Agreguemos las siguientes líneas a nuestra función requestListener():

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
      };
      ...
      

      Esta vez, nuestro Content-Type indica que se está devolviendo un archivo CSV cuyo valor es text/csv. El segundo encabezado que añadimos es Content-Disposition. Este encabezado indica al navegador cómo mostrar los datos, sobre todo en el navegador o como archivo independiente.

      Cuando devolvemos respuestas CSV, la mayoría de los navegadores modernos descargan automáticamente el archivo, incluso si el encabezado Content-Disposition no se ha establecido. Sin embargo, cuando se devuelve un archivo CSV, deberíamos añadir este encabezado, ya que nos permite establecer el nombre del archivo CSV. En este caso, indicamos al navegador que este archivo CSV es un adjunto y debería descargarse. A continuación, indicamos al navegador que el nombre del archivo es oceanpals.csv.

      Vamos a escribir los datos CSV en la respuesta HTTP:

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
          res.writeHead(200);
          res.end(`id,name,emailn1,Sammy Shark,shark@ocean.com`);
      };
      ...
      

      Como antes, devolvemos un estado 200/OK con nuestra respuesta. Esta vez, nuestra invocación de res.end() tiene una cadena que es un CSV válido. La coma separa el valor en cada columna y carácter de nueva línea (n) separa las filas. Tenemos dos, una para el encabezado de tabla y una para los datos.

      Probaremos este servidor en el navegador. Guarde csv.js y salga del editor con CTRL+X.

      Ejecute el servidor con el comando Node.js:

      En otro terminal, vamos a contactar con el servidor usando cURL:

      • curl http://localhost:8000

      La consola mostrará esto:

      Output

      id,name,email 1,Sammy Shark,shark@ocean.com

      Si vamos a http://localhost:8000 en nuestro navegador, se descargará un archivo CSV. Su nombre de archivo será oceanpals.csv.

      Salga del servidor en ejecución con CTRL+C para volver a la solicitud estándar del terminal.

      Al haber devuelto JSON y CSV, cubrimos dos casos que son populares para las APIs. Ahora pasemos a cómo datos para los sitios web que las personas visitan en un navegador.

      Presentar HTML

      HTML, o Lenguaje de marcado de hipertexto, es el formato más común usado cuando queremos que los usuarios interactúen con nuestros servidores a través de un navegador web. Se creó para estructurar el contenido web. Los navegadores web se crearon para mostrar contenido HTML y cualquier estilo que añadamos con CSS, otra tecnología web de front-end que nos permite cambiar la estética de nuestros sitios web.

      Volvamos a abrir html.js con nuestro editor de texto:

      Modifique la función requestListener() para devolver el encabezado Content-Type apropiado para una respuesta HTML:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
      };
      ...
      

      Ahora devolvamos el contenido HTML al usuario. Añada las líneas resaltadas a html.js para que tenga este aspecto:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(`<html><body><h1>This is HTML</h1></body></html>`);
      };
      ...
      

      Primero añadimos el código de estado HTTP. Luego, invocamos response.end() con un argumento de cadena que contiene un HTML válido. Cuando accedamos a nuestro servidor en el navegador, veremos una página HTML con una etiqueta de encabezado que contiene This is HTML.

      Vamos a guardarla y a salir pulsando CTRL+X. Ahora, ejecutaremos el servidor con el comando node:

      Veremos que server se está ejecutando en http://localhost:8000 cuando nuestro programa se ha iniciado.

      Ahora, vaya al navegador y visite http://localhost:8000. Nuestra página tendrá este aspecto:

      Imagen de la respuesta HTML devuelva desde el servidor de Node.js

      Salgamos del servidor en ejecución con CTRL+C para volver a la solicitud estándar del terminal.

      Es común que HTML se escriba en un archivo, separado del código del servidor como nuestros programas Node.js. A continuación, veamos cómo podemos devolver respuestas HTML desde los archivos.

      Paso 3: Presentar una página HTML desde un archivo

      Podemos presentar HTML como cadenas en Node.js para el usuario, pero es preferible que carguemos los archivos HTML y presentemos su contenido. De esta forma, a medida que el archivo HTML crece, no tenemos que mantener cadenas largas en nuestro código Node.js, manteniéndolo más conciso y permitiéndonos trabajar en cada aspecto de nuestro sitio web de forma independiente. Esta “separación de dudas” es común en muchas configuraciones de desarrollo web, y, por tanto, es bueno saber cómo cargar archivos HTML para que sean compatibles en Node.js.

      Para presentar archivos HTML, cargamos el archivo HTML con el módulo fs y usamos sus datos cuando escribamos nuestra respuesta HTTP.

      Primero, crearemos un archivo HTML que el servidor web devolverá. Cree un nuevo archivo HTML:

      Ahora abra index.html en un editor de texto:

      Nuestra página web será mínima. Tendrá un fondo naranja y mostrará algún texto de bienvenida en el centro. Añada este código al archivo:

      first-servers/index.html

      <!DOCTYPE html>
      
      <head>
          <title>My Website</title>
          <style>
              *,
              html {
                  margin: 0;
                  padding: 0;
                  border: 0;
              }
      
              html {
                  width: 100%;
                  height: 100%;
              }
      
              body {
                  width: 100%;
                  height: 100%;
                  position: relative;
                  background-color: rgb(236, 152, 42);
              }
      
              .center {
                  width: 100%;
                  height: 50%;
                  margin: 0;
                  position: absolute;
                  top: 50%;
                  left: 50%;
                  transform: translate(-50%, -50%);
                  color: white;
                  font-family: "Trebuchet MS", Helvetica, sans-serif;
                  text-align: center;
              }
      
              h1 {
                  font-size: 144px;
              }
      
              p {
                  font-size: 64px;
              }
          </style>
      </head>
      
      <body>
          <div class="center">
              <h1>Hello Again!</h1>
              <p>This is served from a file</p>
          </div>
      </body>
      
      </html>
      

      Esta página web muestra dos líneas de texto: Hello Again! y This is served from a file. Las líneas aparecen en el centro de la página, una sobre otra. La primera línea de texto se muestra en un encabezado, lo que significa que será grande. La segunda línea de texto aparecerá ligeramente más pequeña. Todo el texto aparecerá en blanco y la página web tiene un fondo naranja.

      Aunque no está dentro del alcance de este artículo o serie, si está interesado en aprender más sobre HTML, CSS y otras tecnologías web front-end, puede consultar la guía Primeros pasos con la Web de Mozilla.

      Eso es todo lo que necesitamos para el HTML, así que guarde y cierre el archivo con CTRL+X. Ahora podemos pasar al código del servidor.

      Para este ejercicio, trabajaremos en htmlFile.js. Ábralo con el editor de texto:

      Como tenemos que leer un archivo, comenzaremos importando el módulo fs:

      first-servers/htmlFile.js

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

      Este módulo contiene una función readFile() que usaremos para cargar el archivo HTML. Importamos la variable de promesas de acuerdo con las mejores prácticas del JavaScript moderno. Usamos promesas ya que son sintácticamente más precisas que las invocaciones, que tendríamos que usar si asignamos fs a solo require('fs'). Para aprender más sobre las buenas prácticas de la programación asíncrona, puede leer nuestra guía Cómo escribir código asíncrono en Node.js.

      Queremos que nuestro archivo HTML se lea cuando un usuario solicite nuestro sistema. Comencemos modificando requestListener() para leer el archivo:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
      };
      ...
      

      Usamos el método fs.readFile() para cargar el archivo. Su argumento tiene __dirname + "/index.html". La variable especial __dirname tiene la ruta absoluta de donde se está ejecutando el código Node.js. Luego, adjuntamos /index.html para que podamos cargar el archivo HTML que creamos antes.

      Ahora, devolvamos la página HTML cuando se cargue:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
      };
      ...
      

      Si la promesa fs.readFile() se resuelve correctamente, devolverá sus datos. Usamos el método then() para gestionar este caso. El parámetro contents contiene los datos del archivo HTML.

      Primero establecemos el encabezado Content-Type a text/html para indicar al cliente que estamos devolviendo datos HTML. Luego, escribimos el código de estado para indicar que la solicitud se realizó correctamente. Finalmente, enviamos al cliente la página HTML que cargamos, con los datos en la variable contents.

      El método fs.readFile() puede fallar a veces, de modo que deberíamos gestionar este caso cuando obtengamos un error. Añada esto a la función requestListener():

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
              .catch(err => {
                  res.writeHead(500);
                  res.end(err);
                  return;
              });
      };
      ...
      

      Guarde el archivo y salga de nano con CTRL+X.

      Cuando se produce un error con una promesa, se rechaza. Gestionamos ese caso con el método catch(). Acepta el error que fs.readFile() devuelve, establece el código de estado a 500 lo que significa que se produjo un error, y devuelve el error al usuario.

      Ejecute nuestro servidor con el comando node:

      En el navegador web, visite http://localhost:8000. Verá esta página:

      Imagen de página HTML cargada desde un archivo en Node.js

      Ahora devolvió al usuario una página HTML desde el servidor. Puede salir del servidor en ejecución con CTRL+C. Verá de nuevo la solicitud del terminal cuando lo haga.

      Cuando escriba un código como este en producción, es posible que no quiera cargar una página HTML cada vez que reciba una solicitud HTTP. Aunque esta página HTML tiene aproximadamente 800 bytes de tamaño, los sitios web más complejos pueden tener megabytes de tamaño. Los archivos grandes pueden tardar más en cargar. Si su sitio espera mucho tráfico, puede ser mejor cargar los archivos HTML al inicio y guardar su contenido. Tras cargarse, puede configurar el servidor y hacer que escuche las solicitudes en una dirección.

      Para demostrar este método, veamos cómo podemos rediseñar nuestro servidor para que sea más eficiente y escalable.

      Presentar HTML de forma eficiente

      En vez de cargar el HTML para cada solicitud, en este paso lo cargaremos una sola vez al principio. La solicitud devolverá los datos que cargamos al inicio.

      En el terminal, vuelva a abrir la secuencia de comandos Node.js con un editor de textos:

      Vamos a comenzar añadiendo una nueva variable antes de crear la función requestListener():

      first-servers/htmlFile.js

      ...
      let indexFile;
      
      const requestListener = function (req, res) {
      ...
      

      Cuando ejecutemos este programa, esta variable tendrá el contenido del archivo HTML.

      Ahora, volvamos a ajustar la función requestListener(). En vez de cargar el archivo, ahora devolverá el contenido de indexFile:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(indexFile);
      };
      ...
      

      A continuación, cambiamos la lógica de lectura del archivo desde la función requestListener() al inicio de nuestro servidor. Realice los siguientes cambios mientras creamos el servidor:

      first-servers/htmlFile.js

      ...
      
      const server = http.createServer(requestListener);
      
      fs.readFile(__dirname + "/index.html")
          .then(contents => {
              indexFile = contents;
              server.listen(port, host, () => {
                  console.log(`Server is running on http://${host}:${port}`);
              });
          })
          .catch(err => {
              console.error(`Could not read index.html file: ${err}`);
              process.exit(1);
          });
      

      Guarde el archivo y salga de nano con CTRL+X.

      El código que lee el archivo es similar a lo que escribimos en nuestro primer intento. Sin embargo, cuando leemos correctamente el archivo, guardamos el contenido en nuestra variable indexFile global. A continuación, iniciamos el servidor con el método listen(). Lo más importante es que el archivo se cargue antes de ejecutar el servidor. De esta forma, la función requestListener() devolverá seguro una página HTML, ya que indexFile ya no es una variable vacía.

      Nuestro controlador de errores ha cambiado también. Si no se puede cargar el archivo, capturamos el error y lo imprimimos en nuestra consola. A continuación, salimos del programa Node.js con la función exit() sin iniciar el servidor. De esta forma, podemos ver por qué falló la lectura del archivo, resolver el problema e iniciar el servidor de nuevo.

      Ahora hemos creado diferentes servidores web que devuelven varios tipos de datos a un usuario. Hasta ahora, no hemos usado ninguna solicitud de datos para determinar qué deberíamos devolver. Deberemos usar solicitar datos cuando se configuren diferentes rutas en un servidor Node.js, así que veamos cómo funcionan juntos.

      Paso 4: Administrar rutas usando un objeto de solicitud HTTP

      La mayoría de los sitios web que visitamos o APIs que usamos normalmente tienen más de un endpoint, para que podemos acceder a varios recursos. Un buen ejemplo sería un sistema de administración de libros, uno que pueda usarse en una biblioteca. No solo necesitaría administrar los datos de los libros, sino también administrar los datos del autor para facilitar la creación de catálogos y las búsquedas.

      Aunque los datos para libros y autores están relacionados, hay dos objetos diferentes. En estos casos, los desarrolladores de software normalmente codifican cada objeto en endpoints diferentes como una forma de indicar al usuario API con qué tipo de datos está interactuando.

      Crearemos un nuevo servidor para una pequeña biblioteca, que devolverá dos tipos de datos diferentes. Si el usuario va a la dirección de nuestro servidor /books, recibirá una lista de libros en JSON. Si van a /authors, recibirán una lista de información del autor en JSON.

      Hasta ahora, hemos devuelto la misma respuesta a cada solicitud que obtuvimos. Ilustremos esto rápidamente.

      Vuelva a ejecutar nuestro ejemplo de respuesta JSON:

      En otro terminal, vamos a hacer una solicitud cURL como antes:

      • curl http://localhost:8000

      Verá lo siguiente:

      Output

      {"message": "This is a JSON response"}

      Ahora probemos otro comando curl:

      • curl http://localhost:8000/todos

      Tras pulsar Enter, verá el mismo resultado:

      Output

      {"message": "This is a JSON response"}

      No hemos creado ninguna lógica especial en nuestra función requestListener() para gestionar una solicitud cuya URL contiene /todos, de forma que Node.js devuelve el mismo mensaje JSON por defecto.

      Ya que queremos crear un servidor de gestión de una biblioteca en miniatura, ahora separaremos los tipos de datos que se devuelven en base al endpoint al que el usuario accede.

      Primero, salga del servidor en ejecución con CTRL+C.

      Ahora, abra routes.js en su editor de texto:

      Comenzaremos almacenando nuestros datos JSON en variables antes de la función requestListener():

      first-servers/routes.js

      ...
      const books = JSON.stringify([
          { title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
          { title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
      ]);
      
      const authors = JSON.stringify([
          { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
          { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
      ]);
      ...
      

      La variable books es una cadena que contiene JSON para una variedad de objetos de libros. Cada libro tiene un título o nombre, un autor, y el año en que se publicó.

      La variable authors es una cadena que contiene el JSON para diversos objetos de autor. Cada autor tiene un nombre, un país de nacimiento y su año de nacimiento.

      Ahora que tenemos los datos que nuestras respuestas devolverán, vamos a comenzar a modificar la función requestListener() para devolverlas a las rutas correctas.

      Primero, nos aseguraremos de que cada respuesta desde nuestro servidor tenga el encabezado Content-Type correcto:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      }
      ...
      

      Ahora, queremos devolver el JSON adecuado, dependiendo de la ruta URL que el usuario visite. Crearemos la instrucción switch en la URL de la solicitud:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {}
      }
      ...
      

      Para obtener la ruta URL desde un objeto de solicitud, debemos acceder a su propiedad url. Ahora podemos añadir casos a la instrucción switch para devolver el JSON apropiado.

      La instrucción switch de JavaScrip proporciona una forma de controlar qué código se ejecuta dependiendo del valor de un objeto o expresión JavaScript (por ejemplo, el resultado de operaciones matemáticas). Si necesita una lección o un recordatorio sobre cómo usarlas, eche un vistazo a nuestra guía sobre Cómo usar la instrucción Switch en JavaScript.

      Ahora, agreguemos un caso para cuando el usuario quiera obtener nuestra lista de libros:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
          }
      }
      ...
      

      Establecemos nuestro código de estado a 200 para indicar que la solicitud está bien y devolver el JSON que contiene la lista de nuestros libros. Ahora, vamos a añadir otro caso para nuestro autores:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
          }
      }
      ...
      

      Como antes, el código de estado será 200, ya que la solicitud está bien. Esta vez, devolvemos el JSON que contiene la lista de nuestros autores.

      Queremos devolver un error si el usuario intenta ir a cualquier otra ruta. agreguemos el caso predeterminado para hacer esto:

      routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
              default:
                  res.writeHead(404);
                  res.end(JSON.stringify({error:"Resource not found"}));
          }
      }
      ...
      

      Usamos la palabra clave default en una instrucción switch para capturar todos los otros escenarios no capturados por nuestros casos previos. Establecemos el código de estado a 404 para indicar que no se encontró la URL que estaban buscando. A continuación, añadimos un objeto JSON que contiene un mensaje de error.

      Probemos nuestro servidor para ver si se comporta como esperamos. En otro terminal, primero vamos a ejecutar un comando para ver si obtenemos nuestra lista de libros:

      • curl http://localhost:8000/books

      Pulse Enter para ver el siguiente resultado:

      Output

      [{"title":"The Alchemist","author":"Paulo Coelho","year":1988},{"title":"The Prophet","author":"Kahlil Gibran","year":1923}]

      Hasta ahora, todo bien. Intentemos lo mismo para /authors. Escriba el siguiente comando en el terminal:

      • curl http://localhost:8000/authors

      Verá el siguiente resultado cuando el comando se complete:

      Output

      [{"name":"Paulo Coelho","countryOfBirth":"Brazil","yearOfBirth":1947},{"name":"Kahlil Gibran","countryOfBirth":"Lebanon","yearOfBirth":1883}]

      Por último, probemos otra URL errónea para asegurarnos de que requestListener() devuelve la respuesta del error:

      • curl http://localhost:8000/notreal

      Al introducir ese comando, se mostrará este mensaje:

      Output

      {"error":"Resource not found"}

      Puede salir del servidor en ejecución con CTRL+C.

      Ahora hemos creado diferentes caminos para que nuestros usuarios obtengan datos diferentes. También hemos añadido una respuesta predeterminada que devuelve un error HTTP si el usuario introduce una URL que no admitimos.

      Conclusión

      En este tutorial, creó una serie de servidores HTTP Node.js. Primero devolvió una respuesta textual básica. Luego, devolvió varios tipos de datos desde nuestro servidor: JSON, CSV y HTML. Desde ahí, pudo combinar la carga de archivos con las respuestas HTTP para devolver una página HTML desde un servidor al usuario, y para crear una API que usó información sobre la solicitud del usuario para determinar qué datos deberían enviarse en su respuesta.

      Ahora está preparado para crear servidores web que pueden gestionar una variedad de solicitudes y respuestas. Con este conocimiento, puede crear un servidor que devuelva muchas páginas HTML al usuario en diferentes puntos finales. También podría crear su propia API.

      Para obtener más información sobre los servidores web HTTP en Node.js, puede leer la documentación sobre Node.js en el módulo http. Si desea seguir aprendiendo sobre Node.js, puede volver a la página de la serie Cómo desarrollar código en Node.js.



      Source link

      Cómo probar un módulo de Node.js con Mocha y Assert


      El autor seleccionó a Open Internet/Free Speech Fund para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Las pruebas son una parte integral del desarrollo de software. Es común para los programadores ejecutar código para probar su aplicación a medida que realizan cambios y así confirmar que se se comporta como se espera. Con la configuración de prueba correcta, este proceso puede incluso automatizarse y ahorrar mucho tiempo. Ejecutar pruebas de manera uniforme tras escribir nuevo código garantiza que los nuevos cambios no afecten las funciones preexistentes. Esto proporciona al desarrollador confianza en su base de código, en especial cuando se implementa en la producción para que los usuarios puedan interactuar con ella.

      Un marco de prueba estructura la forma en que creamos casos de prueba. Mocha es un marco de prueba de JavaScript muy popular que organiza nuestros casos de prueba y se encarga de ejecutarlos. Sin embargo, no verifica el comportamiento de nuestro código. Para comparar los valores en una prueba, podemos usar el módulo assert de Node.js.

      En este artículo, escribirá pruebas para un módulo de lista TODO de Node.js. Configurará y usará el marco de prueba Mocha para estructurar sus pruebas. A continuación, usará el módulo assert Node.js para crear las pruebas propiamente dichas. En este sentido, usará Mocha como creador del plan, y assert para implementar el plan.

      Requisitos previos

      Paso 1: Escribir un módulo Node

      Nuestro primer paso para este artículo será escribir el módulo Node.js que nos gustaría probar. Este módulo administrará una lista de elementos TODO. Con este módulo podremos enumerar todos los elementos TODO de los que realizamos un seguimiento, añadir nuevos elementos y marcar algunos como completados. Además, podrá exportar una lista de elementos TODO a un archivo CSV. Si desea repasar la manera de escribir módulos de Node.js, puede leer nuestro artículo Cómo crear un módulo de Node.js.

      Primero, debemos configurar el entorno de codificación. Cree una carpeta con el nombre de su proyecto en su terminal. En este tutorial se usará el nombre todos:

      Luego acceda a esa carpeta:

      Ahora, inicie npm. Usaremos su funcionalidad CLI para ejecutar las pruebas más tarde:

      Solo tenemos una dependencia, Mocha, que usaremos para organizar y ejecutar nuestras pruebas. Para descargar e instalar Mocha, utilice lo siguiente:

      • npm i request --save-dev mocha

      Instalamos Mocha como dependencia dev, ya que en un entorno de producción el módulo no lo necesita. Si desea obtener más información sobre los paquetes de Node.js o npm, consulte nuestra guía Cómo usar módulos Node.js con npm y package.json.

      Finalmente, crearemos nuestro archivo que contendrá el código de nuestro módulo:

      Con esto, estaremos listos para crear nuestro módulo. Abra index.js en un editor de texto; por ejemplo, nano:

      Comenzaremos definiendo la clase Todos. Esta clase contiene todas las funciones que necesitamos para administrar nuestra lista TODO. Añada las siguientes líneas de código a index.js:

      todos/index.js

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

      Comenzamos el archivo creando una clase Todos. Su función constructor() no toma argumentos. Por lo tanto, no es necesario proporcionar valores para crear instancias de un objeto para esta clase. Lo único que hacemos cuando iniciamos un objeto Todos es crear una propiedad todos, que es una matriz vacía.

      La línea modules permite que otros módulos de Node.js requieran nuestra clase Todos. Si no se exporta explícitamente la clase, el archivo de prueba que crearemos más tarde no podrá usarla.

      Añadiremos una función para mostrar la matriz de todos que almacenamos. Escriba las siguientes líneas resaltadas:

      todos/index.js

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

      Nuestra función list() devuelve una copia de la matriz usada por la clase. Hace una copia de la matriz usando la sintaxis de desestructuración de JavaScript. Creamos una copia de la matriz de modo que los cambios que el usuario realice en la matriz y se muestren a través de list() no afecten a la matriz usada por el objeto Todos.

      Nota: Las matrices de JavaScript son tipos de referencia. Esto significa que para cualquier asignación de variable a una matriz o invocación de función con una matriz como parámetro, JavaScript consulta la matriz original que se creó. Por ejemplo, si tenemos una matriz con tres elementos llamada x, y creamos una nueva variable y de modo que y = x, y y x hacen referencia a lo mismo. Cualquier cambio que realicemos a la matriz con y afecta a la variable x y viceversa.

      Ahora escribiremos la función add(), que añade un nuevo elemento TODO:

      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;
      

      Nuestra función add() toma una cadena y la dispone en una nueva propiedad title del objeto de JavaScript. El nuevo objeto también tiene una propiedad completed, que se fija en false por defecto. Luego añadimos este nuevo objeto a nuestra matriz de TODO.

      Una funcionalidad importante en un administrador de TODO es la de marcar los elementos como completados. Para esta implementación, repetiremos nuestra matriz todos para hallar el elemento TODO que el usuario está buscando. Si se encuentra uno, lo marcaremos como completado. Si no se encuentra ninguno, mostraremos un error.

      Añada la función complete() de esta 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;
      

      Guarde el archivo y cierre el editor de texto.

      Ahora disponemos de un administrador TODO básico con el que podemos experimentar. A continuación, probaremos nuestro código manualmente para ver si la aplicación funciona.

      Paso 2: Probar el código de forma manual

      En este paso, ejecutaremos las funciones de nuestro código y observaremos el resultado para garantizar que cumpla con nuestras expectativas. Esto se conoce como prueba manual. Es probablemente la metodología de prueba más común que aplican los programadores. Aunque automatizaremos nuestra prueba más tarde con Mocha, primero probaremos manualmente nuestro código para dar un mejor sentido a cómo la prueba manual difiere de los marcos de prueba.

      Añadiremos dos elementos TODO a nuestra aplicación y marcaremos uno como completado. Inicie el REPL de Node.js en la misma carpeta que el archivo index.js:

      Verá el símbolo > en el REPL que dice que podemos ingresar código de JavaScript. Escriba lo siguiente en el símbolo:

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

      Con require(), cargamos el módulo TODOs en una variable Todos. Recuerde que nuestro módulo muestra la clase Todos por defecto.

      Ahora, vamos a instanciar un objeto para esa clase. En el REPL, añada esta línea de código:

      • const todos = new Todos();

      Podemos usar el objeto todos para verificar que nuestra implementación funcione. Añadiremos nuestro primer elemento TODO:

      Hasta ahora, no vimos ningún resultado en nuestro terminal. Verificaremos que almacenamos nuestro elemento TODO "run code" obteniendo una lista de todos nuestros TODO:

      Verá este resultado en su REPL:

      Output

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

      Este es el resultado esperado: tenemos un elemento TODO en nuestra matriz de TODO y no está completado por defecto.

      Vamos añadiremos otro elemento TODO:

      • todos.add("test everything");

      Marque el primer elemento TODO como completado:

      • todos.complete("run code");

      Nuestro objeto todos ahora estará administrando dos elementos: "run code" y "test everything". El TODO "run code" también se completará. Confirmaremos esto invocando list() de nuevo:

      El REPL mostrará el siguiente resultado:

      Output

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

      Ahora, salga del REPL con lo siguiente:

      Hemos confirmado que nuestro módulo se comporta como esperamos. Aunque no dispusimos nuestro código en un archivo de prueba o usamos una biblioteca de prueba, probamos nuestro código manualmente. Desafortunadamente, esta forma de prueba requiere mucho tiempo cada vez que se realiza un cambio. A continuación, utilizaremos las pruebas automatizadas en Node.js y veremos si podemos resolver este problema con el marco de prueba Mocha.

      Paso 3: Escribir su primera prueba con Mocha y Assert

      En el último paso, probamos nuestra aplicación de forma manual. Esto funcionará para los casos de uso individuales, pero a medida que nuestro módulo se amplía, este método se vuelve menos viable. A medida que probemos nuevas funciones, debemos estar seguros de que la funcionalidad añadida no genere problemas con la funcionalidad anterior. Nos gustaría probar cada función una y otra vez para cada cambio del código, pero hacer esto a mano tomaría mucho esfuerzo y podría producir a errores.

      Una práctica más eficiente consistiría en configurar pruebas automatizadas. Estas son pruebas con secuencias de comandos escritas como cualquier otro bloque de código. Ejecutamos nuestras funciones con entradas definidas e inspeccionamos sus efectos para garantizar que se comporten como esperamos. A medida que nuestra base de código se amplíe, lo mismo sucederá con nuestras pruebas automatizadas. Cuando escribimos nuevas pruebas junto a las funciones, podemos verificar que el módulo completo aún funcione, todo sin necesidad de recordar la manera en que se usa cada función en cada ocasión.

      En este tutorial, usaremos el marco de pruebas Mocha con el módulo assert de Node.js. Recurriremos a la experiencia práctica para ver cómo funcionan juntos.

      Para comenzar, cree un nuevo archivo para almacenar nuestro código de prueba:

      Ahora, utilice su editor de texto para abrir el archivo de prueba. Puede usar nano como antes:

      En la primera línea del archivo de texto, cargaremos el módulo TODO como hicimos en el shell de Node.js. Luego cargaremos el módulo assert para cuando escribamos nuestras pruebas. Añada las siguientes líneas:

      todos/index.test.js

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

      La propiedad strict del módulo assert nos permitirá usar las pruebas de igualdad especiales que se recomiendan desde Node.js y son adecuadas para una buena protección futura, ya que tienen en cuenta la mayoría de los casos de uso.

      Antes de pasar a las pruebas de escritura, veremos cómo Mocha organiza nuestro código. Las pruebas estructuradas en Mocha normalmente siguen esta plantilla:

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

      Observe las dos funciones principales: describe() e it(). La función describe() se usa para agrupar pruebas similares. No es necesario para que Mocha ejecute las pruebas, pero agrupar las pruebas facilita el mantenimiento de nuestro código. Se recomienda que agrupe sus pruebas de una manera que le permita actualizar fácilmente las que son similares.

      it() contiene nuestro código de prueba. Aquí interactuaremos con las funciones de nuestro módulo y usaremos la bibliteca assert. Muchas funciones it() pueden definirse en una función describe().

      Nuestro objetivo en esta sección es usar Mocha y assert para automatizar nuestra prueba manual. Haremos esto paso a paso, comenzando con nuestro bloque de descripción. Añada lo siguiente a su archivo después de las líneas del módulo:

      todos/index.test.js

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

      Con este bloque de código, hemos creado una agrupación para nuestras pruebas integradas. Las pruebas Unit probarán una función a la vez. Las pruebas de integración verifican si las funciones en o entre los módulos funcionan bien juntas. Cuando Mocha ejecute nuestras pruebas, todas las pruebas en ese bloque de descripción se ejecutarán en el grupo "integration test".

      Añadiremos una función it() para que podamos comenzar a probar el código de nuestro módulo.

      todos/index.test.js

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

      Observe que hicimos que el nombre de la prueba sea descriptivo. Si alguien ejecuta nuestra prueba, quedará claro de inmediato qué supera la prueba y qué no. Una aplicación bien probada normalmente es una aplicación bien documentada, y las pruebas pueden ser a veces un tipo de documentación efectivo.

      Para nuestra primera prueba, crearemos un nuevo objeto Todos y verificaremos que no tenga elementos.

      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);
          });
      });
      

      La primera nueva línea de código creó una instancia de un nuevo objeto Todos como haríamos en el REPL de Node.js o en otro módulo. En la segunda nueva línea, usamos el módulo assert.

      Desde el módulo assert usamos el método notStrictEqual(). Esta función toma dos parámetros: el valor que deseamos probar (llamado actual) y el valor que esperamos obtener (llamado expected). Si ambos argumentos son iguales, notStrictEqual() genera un error para que la prueba falle.

      Guarde y cierre index.test.js.

      El caso básico será true, ya que la extensión debería ser 0, que no es 1. Vamos a confirmar esto ejecutando Mocha. Para hacer esto, debemos modificar nuestro archivo package.json. Abra el archivo package.json con su editor de texto:

      Ahora, en su propiedad scripts, cámbielo para que tenga este aspecto:

      todos/package.json

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

      Acabamos de cambiar el comportamiento del comando CLI test de npm. Cuando ejecutemos npm test, npm revisará el comando que acabamos de introducir en package.json. Buscará la biblioteca Mocha en nuestra carpeta node_modules y ejecutará el comando mocha con nuestro archivo de prueba.

      Guarde y cierre package.json.

      Veamos qué sucede cuando ejecutemos nuestra prueba. En su terminal, introduzca lo siguiente:

      El comando producirá el siguiente 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)

      Este resultado primero nos muestra el grupo de pruebas que está a punto de ejecutarse. Para cada prueba individual en un grupo, se marca el caso de prueba. Vemos el nombre de nuestra prueba como lo describimos en la función it(). La marca de verificación en el lado izquierdo del caso de prueba indica que se superó la prueba.

      En la parte inferior, vemos un resumen de todas nuestras pruebas. En nuestro caso, nuestra prueba fue superada y se completó en 16 ms (el tiempo varía según el equipo).

      Nuestra prueba se inició correctamente. Sin embargo, este caso de prueba actual puede dar lugar a instancias de “falsos positivos”. Un falso positivo es un caso en el que se supera la prueba cuando esto no debería suceder.

      Actualmente comprobamos que la extensión de la matriz no sea igual a 1. Modificaremos la prueba de modo que esta condición sea “true” cuando este no debería ser el caso. Añada las siguientes líneas a 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);
          });
      });
      

      Guarde el archivo y ciérrelo.

      Añadimos dos elementos TODO. Ejecutaremos la prueba para ver qué sucede:

      Esto dará el siguiente resultado:

      Output

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

      La prueba se supera, como se espera, ya que la extensión es mayor a 1. Sin embargo, se frustra la finalidad original de contar con esa primera prueba. La primera prueba está destinada a confirmar que empezamos en un estado vacío. Una mejor prueba confirmará eso en todos los casos.

      Cambiaremos la prueba de modo que solo se supere si no tenemos ningú´n TODO. Aplique los siguientes cambios en 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);
          });
      });
      

      Cambió notStrictEqual() por strictEqual(), una función que comprueba la igualdad entre su argumento real y esperado. “strictEqual” fallará si nuestros argumentos no son exactamente los mismos.

      Guarde y cierre el archivo, y ejecute la prueba para que podamos ver lo que sucede:

      Esta vez, en el resultado se mostará un error:

      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 nos resultará útil para determinar por qué falló la prueba mediante depuración. Observe que, dado que la prueba falló, no apareció una marca de verificación al principio del caso de prueba.

      Nuestro resumen de prueba ya no está en la parte inferior del resultado, sino justo después de donde nuestra lista de casos de prueba se mostraba:

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

      El resultado restante nos proporciona datos sobre las pruebas no superadas. Primero, vemos el caso de prueba que falló:

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

      A continuación, vemos por qué falló nuestra prueba:

      ...
            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)
      ...
      

      Se presenta un AssertionError cuando strictEqual() falla. Vemos que el valor expected, 0, es diferente el valor actual.

      A continuación vemos la línea en nuestro archivo de prueba en el que falla el código. En este caso, es la línea 10.

      Ahora, vimos que nuestra prueba fallará si esperamos valores incorrectos. Cambiaremos nuestro caso de prueba de nuevo a su valor correcto. Primero, abra el archivo:

      A continuación, elimine las líneas todos.add de modo que su código tenga el siguiente aspecto:

      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);
          });
      });
      

      Guarde el archivo y ciérrelo.

      Ejecútelo una vez más para confirmar que supere la prueba sin posibles instancias de “falsos positivos”:

      El resultado será el siguiente:

      Output

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

      Con esto, mejoramos bastante la resistencia de nuestra prueba. Continuaremos con nuestra prueba de integración. El siguiente paso es añadir un nuevo elemento TODO a 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}]);
          });
      });
      

      Tras utilizar la función add(), confirmamos que ahora tenemos un TODO administrado por nuestro objeto todos con strictEqual(). Nuestra siguiente prueba confirma los datos en todos con deepStrictEqual(). La función deepStrictEqual() prueba de forma recursiva que nuestros objetos esperados y reales tienen las mismas propiedades. En este caso, prueba que las matrices que esperamos tengan un objeto JavaScript en ellas. Luego comprueba que sus objetos JavaScript tengan las mismas propiedades; es decir, que sus propiedades titles sean "run code" y sus propiedades completed sean false.

      A continuación, completaremos las pruebas restantes con estas dos comprobaciones de igualdad, según sea necesario, añadiendo las siguientes líneas resaltadas:

      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 }
                  ]
          );
        });
      });
      

      Guarde el archivo y ciérrelo.

      Nuestra prueba ahora imita a nuestra prueba manual. Con estas pruebas mediante programación, no es necesario comprobar el resultado continuamente si nuestras pruebas son superadas cuando las ejecutamos. Normalmente, le convendrá probar todos los aspectos de uso para garantizar que el código se pruebe adecuadamente.

      Ejecutaremos nuestra prueba con npm test una vez más para obtener este resultado familiar:

      Output

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

      Con esto, habrá configurado una prueba integrada con el marco Mocha y la biblioteca assert.

      Consideraremos una situación en la que haya compartido nuestro módulo con otros desarrolladores y ahora nos den su opinión. Una buena parte de nuestros usuarios desearán que la función complete() muestre un error si no se añadieron TODO aún. Añadiremos esta funcionalidad en nuestra función complete().

      Abra index.js en su editor de texto:

      Añada lo siguiente a la función:

      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}"`);
          }
      }
      ...
      

      Guarde el archivo y ciérrelo.

      Ahora, añadiremos una nueva prueba para esta nueva función. Queremos verificar que si invocamos “completed” en un objeto Todos que no tiene elementos, mostrará nuestro error especial.

      Vuelva a index.test.js:

      Al final del archivo, añada el siguiente 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 describe() y it() como antes. Nuestra prueba comienza con la creación de un nuevo objeto todos. A continuación, definimos el error que estamos esperando ver cuando invocamos la función complete().

      A continuación, usamos la función throws() del módulo assert. Esta función se creó para que podamos verificar los errores que se producen en nuestro código. Su primer argumento es una función que contiene el código que produce el error. El segundo argumento es el error que estamos esperando ver.

      En su terminal, ejecute las pruebas con npm test de nuevo. Ahora verá el siguiente resultado:

      Output

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

      En este resultado se resalta el beneficio que nos trae realizar pruebas automatizadas con Mocha y assert. Debido a que nuestras pruebas están programadas, cada vez que ejecutamos npm test verificamos que todas nuestras pruebas se superan. No fue necesario comprobar manualmente si el otro código aún funciona; sabemos que es así porque las pruebas que tenemos se han superado.

      Hasta ahora, nuestras pruebas han verificado los resultados del código sincronizado. Vamos a ver cómo deberíamos adaptar nuestros nuevos hábitos de prueba para que funcionen con el código asíncrono.

      Paso 4: Probar código asíncrono

      Una de las funciones que deseamos en nuestro módulo TODO es una función de exportación de CSV. Imprimirá en un archivo todos los TODO que tenemos guardados junto con el estado “completed”. Esto requiere que usemos el módulo fs: un módulo de Node.js integrado para trabajar con el sistema de archivos.

      La escritura en un archivo es una operación asíncrona. Existen muchas formas de escribir en un archivo en Node.js. Podemos usar callbacks, promesas o las palabras claves async/await. En esta sección, veremos la manera de escribir pruebas para esos métodos diferentes.

      Callbacks

      Una función callback es la que se utiliza como argumento para una función asíncrona. Se invoca cuando se completa la operación asíncrona.

      Añadiremos una función a nuestra clase Todos llamada saveToFile(). Esta función creará una cadena realizando un bucle con todos nuestros elementos TODO y escribiendo esa cadena en un archivo.

      Abra su archivo index.js:

      En este archivo, añada el siguiente código resaltado:

      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;
      

      Primero tenemos que importar el módulo fs en nuestro archivo. Luego añadimos nuestra nueva función saveToFile(). Nuestra función toma una función callback que se utilizará una vez que se complete la operación de escritura del archivo. En esa función, creamos una variable fileContents que almacena toda la cadena que queremos guardar como archivo. Se inicializa con los encabezados de CSV. A continuación, realizamos un bucle con cada elemento TODO con el método forEach() de la matriz interna. A medida que realizamos iteraciones, añadimos las propiedades title y completed de los objetos todos individuales.

      Finalmente, usamos el módulo fs para escribir el archivo con la función writeFile(). Nuestro primer argumento es el nombre del archivo: todos.csv. El segundo es el contenido del archivo. En este caso, nuestra variable fileContents. Nuestro último argumento es nuestra función callback, que gestiona cualquier error de escritura.

      Guarde el archivo y ciérrelo.

      Ahora escribiremos una prueba para nuestra función saveToFile(). Nuestra prueba hará dos acciones: confirmar que el archivo existe en primer lugar y luego verificar que su contenido sea correcto.

      Abra el archivo index.test.js:

      Comenzaremos cargando el módulo fs en la parte superior del archivo, ya que lo usaremos para ayudar a probar nuestros resultados:

      todos/index.test.js

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

      Ahora, al final del archivo, añadiremos nuestro caso de prueba:

      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);
              });
          });
      });
      

      Como antes, usamos describe() para agrupar nuestras pruebas por separado respecto de las otras, ya que implica una nueva funcionalidad. La función it() se diferencia ligeramente de las otras. Normalmente, la función callback que usamos no tiene argumentos. Esta vez, tenemos done como argumento. Necesitamos este argumento cuando probemos funciones con callbacks. Mocha utiliza la función de callback done() para indicarle cuando se completa una función asíncrona.

      Todas las funciones de callback probadas en Mocha deben invocar el callback done(). Si no es así, Mocha nunca sabría cuándo se completó la función y se quedaría esperando una señal.

      Para continuar, creamos nuestra instancia de Todos y añadimos un único elemento a ella. Luego, invocamos la función saveToFile() con un callback que captura un error de escritura de archivo. Observe que nuestra prueba para esta función reside en el callback. Si nuestro código de prueba estuviera fuera del callback, fallaría siempre que se invocase antes de completar la escritura del archivo.

      En nuestra función de callback, primero comprobamos que nuestro archivo existe:

      todos/index.test.js

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

      La función fs.existsSync() muestra true si la ruta del archivo en su argumento existe; de lo contrario, será false.

      Nota: Las funciones del módulo fs son asíncronas por defecto. Sin embargo, para las funciones claves, crean equivalentes síncronos. Esta prueba es más sencilla al usar las funciones síncronas, ya que no necesitamos anidar el código asíncrono para garantizar que funcione. En el módulo fs, las funciones síncronas normalmente terminan con "Sync" al final de sus nombres.

      A continuación, crearemos una variable para guardar nuestro valor esperado:

      todos/index.test.js

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

      Usamos readFileSync() del módulo fs para leer el archivo de forma síncrona:

      todos/index.test.js

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

      Ahora, proporcionamos readFileSync() con la ruta correcta para el archivo: todos.csv. A medida que readFileSync() muestre el objeto Buffer, que almacena datos binarios, usamos su método toString() para poder comparar su valor con la cadena que esperamos tener guardada.

      Como antes, usamos el strictEqual del módulo assert para realizar una comparación:

      todos/index.test.js

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

      Terminamos nuestra prueba invocando el callback done() y garantizamos que Mocha sepa detener la prueba de ese caso:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Proporcionamos el objeto err a done() de modo que Mocha pueda fallar en la prueba en caso de que se produzca un error.

      Guarde y cierre index.test.js.

      Ejecutaremos esta prueba con npm test como antes. En su consola se mostrará 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)

      De esta manera, habrá probado su primera función asíncrona con Mocha usando callbacks. Sin embargo, en el momento en que se redactó este tutorial las promesas eran más prevalentes que los callbacks en el nuevo código Node.js, como se explica en nuestro artículo Cómo escribir código asíncrono en Node.js. A continuación, aprenderá a pobarlos con Mocha también.

      Promesas

      Una promesa es un objeto de JavaSript que, llegado el momento, mostrará un valor. Cuando una promesa se realiza correctamente, se resuelve. Cuando en ella se produce un error, se rechaza.

      Modificaremos la función saveToFile() para que utilice promesas en vez de callbacks. Abra index.js:

      Primero, debemos cambiar la forma en que se carga el módulo fs. En su archivo index.js, cambie la instrucción require() en la parte superior del archivo de modo que tenga este aspecto:

      todos/index.js

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

      Acabamos de importar el módulo fs, que usa promesas en vez de callbacks. Ahora, debemos realizar algunos cambios en saveToFile() para que funcione con promesas.

      En su editor de texto, aplique los siguientes cambios a la función saveToFile() para eliminar los 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);
      }
      ...
      

      La primera diferencia es que nuestra función ya no acepta ningún argumento. Con promesas no necesitamos una función de callback. El segundo cambio hace referencia a la forma en que se escribe el archivo. Ahora, devolvemos el resultado de la promesa writeFile().

      Guarde y cierre index.js.

      Ahora adaptaremos nuestra prueba de modo que funcione con promesas. Abra index.test.js:

      Cambie la prueba saveToFile() por lo siguiente:

      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);
              });
          });
      });
      

      El primer cambio que debemos realizar es eliminar el callback done() de sus argumentos. Si Mocha pasa el argumento done(), debe invocarse. De lo contrario, generará un error como este:

      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)
      

      Al probar promesas, no incluya el callback done() en it().

      Para probar nuestra promesa, debemos disponer nuestro código de aserción en la función then(). Observe que mostramos esta promesa en la prueba y que no tenemos una función catch() que capturar cuando se rechaza la Promise.

      Mostramos la promesa para que cualquier error que aparezca en la función then() se extienda a la función it(). Si los errores no aparecen, Mocha no hará que el caso de prueba fracase. Al probar promesas, deberá usar return en la Promise que se esté probando. Si no es así, tendrá el riesgo de obtener una instancia de falso positivo.

      También omitimos la clausula catch() porque Mocha puede detectar los casos en que se rechaza una promesa. Si se rechaza, automáticamente no superará la prueba.

      Ahora que nuestra prueba está lista, guarde y cierre el archivo y luego ejecute Mocha con npm test, también para confirmar que el resultado sea satisfactorio:

      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)

      Cambianis nuestro código y la prueba para usar promesas y ahora sabemos con seguridad que funciona. Sin embargo, los patrones asíncronos más recientes utilizan las palabras claves async y await a fin de que no tengamos que crear varias funciones then() para gestionar resultados satisfactorios. Vamos a ver cómo podemos hacer la prueba con async y await.

      async/await

      Las palabras claves async y await hacen que trabajar con promesas sea menos complicado. Una vez que definimos una función como asíncrona con la palabra clave async, podemos obtener cualquier resultado futuro en esa función con la palabra clave await. De esta forma, podemos usar promesas sin tener que usar las funciones then() o catch().

      Podemos simplificar nuestra prueba saveToFile() basada en promesas con async y await. En su editor de texto, realice estas pequeñas ediciones en la prueba saveToFile() en 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);
          });
      });
      

      El primer cambio es que la función usada por la función it() ahora tiene la palabra clave async cuando se define. Esto nos permite usar la palabra clave await dentro de su cuerpo.

      El segundo cambio se encuentra cuando invocamos saveToFile(). La palabra clave await se usa antes de se invocación. Ahora, Node.js sabe que debe esperar hasta que se resuelva esta función antes de continuar con la prueba.

      Nuestro código de función es más fácil de leer ahora que movimos el código que estaba en la función then() al cuerpo de la función it(). Ejecutar este código con npm test produce 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)

      Ahora podemos probar las funciones asíncronas usando cualquiera de estos paradigmas asíncronos de forma apropiada.

      Abarcamos un terreno amplio con las pruebas de código síncrono y asíncrono a través de Mocha. A continuación, profundizar en otras funcionalidades que ofrece Mocha para mejorar nuestra experiencia de prueba; en particular, la forma en que los enlaces pueden cambiar entornos de prueba.

      Paso 5: Usar enlaces para mejorar casos de prueba

      Los enlaces son una función útil de Mocha que nos permite configurar el entorno antes y después de una prueba. Normalmente, añadimos hooks en un bloque de funciones describe(), ya que contienen lógica de configuración y desglose específica para algunos casos de prueba.

      Mocha ofrece cuatro enlaces que podemos usar en nuestras pruebas:

      • before: este enlace se ejecuta una vez antes de que se inicie la primera prueba.
      • beforeEach: este se ejecuta antes de cada caso de prueba.
      • after: este se ejecuta una vez después que se completa el último caso de prueba.
      • afterEach: este se ejecuta después de cada caso de prueba.

      Cuando probamos una función o característica varias veces, los enlaces son útiles, ya que nos permiten separar el código de configuración de la prueba (como la creación del objeto todos) desde el código de aserción de esta.

      Para ver el valor de los enlaces, añadiremos más pruebas a nuestro bloque de pruebas saveToFile().

      Aunque confirmamos que podemos guardar nuestros elementos TODO en un archivo, solo guardamos un elemento. Además, el elemento no se marcó como completado. Añadiremos más pruebas para asegurarnos de que los diferentes aspectos de nuestro módulo funcionen.

      Primero, añadiremos una segunda prueba para confirmar que nuestro archivo se guarde correctamente cuando hayamos completado un elemento TODO. Abra el archivo index.test.js en su editor de texto:

      Cambie la última prueba para obtener lo siguiente:

      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);
          });
      });
      

      La prueba es similar a lo que teníamos antes. Las principales diferencias radican en que invocamos la función complete() antes de invocar saveToFile() y que ahora el valor de nuestros expectedFileContents es true en vez de false para el valor de la columna completed.

      Guarde el archivo y ciérrelo.

      Ejecutaremos nuestra nueva prueba y todas las demás con npm test:

      Esto dará el siguiente 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 ✓ should save a single TODO that's completed 4 passing (26ms)

      Funciona como se espera. Sin embargo, esto se puede mejorar. Ambas tienen que crear una instancia de un objeto Todos al principio de la prueba. A medida que añadimos más casos de prueba, esto rápidamente se vuelve repetitivo y desperdicia mucha memoria. Además, cada vez que ejecutamos la prueba se crea un archivo. Alguien menos familiarizado con el módulo puede confundir esto con un resultado real. Estaría bien que limpiásemos nuestros archivos resultantes tras la prueba.

      Realizaremos estas mejoras usando enlaces de pruebas. Usaremos el enlace beforeEach() para configurar nuestro artefacto de prueba de elementos TODO. Un artefacto de prueba es cualquier estado uniforme que se usa en una prueba. En nuestro caso, nuestro artefacto de prueba es un nuevo objeto todos que tiene un elemento TODO ya añadido. A continuación usaremos afterEach() para eliminar el archivo creado por la prueba.

      En index.test.js, realice los siguientes cambios a su última prueba para 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);
          });
      });
      

      Desglosaremos todos los cambios que realizamos. Añadimos un bloque beforeEach() al bloque de pruebas:

      todos/index.test.js

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

      Estas dos líneas de código crean un nuevo objeto Todos que estará disponible en cada una de nuestras pruebas. Con Mocha, el objeto this en beforeEach() hace referencia al mismo objeto this en it(). this es el mismo en todos los bloques de código dentro del bloque describe(). Para obtener más datos sobre this, consulte nuestro tutorial Información sobre This, Bind, Call y Apply en JavaScript.

      Este potente intercambio de contexto es el motivo por el que podemos crear rápidamente artefactos que funcionarán para nuestras dos pruebas.

      A continuación, limpiaremos nuestro archivo CSV en la función afterEach():

      todos/index.test.js

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

      Si nuestra prueba falló, es posible que no haya creado un archivo. Por eso verificamos si el archivo existe antes de usar la función unlinkSync() para eliminarlo.

      Los cambios restantes cambian la referencia de todos, creados previamente en la función it(), a this.todos, que está disponible en el contexto de Mocha. También eliminamos las líneas que previamente crearon instancias de todos en los casos de prueba individuales.

      Ahora, ejecutaremos este archivo para confimar que nuestras pruebas aún funcionen. Introduzca npm test en su terminal para obtener lo siguiente:

      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)

      Los resultados son los mismos y, como beneficio, redujimos ligeramente el tiempo de configuración para las nuevas pruebas de la función saveToFile() y encontramos una solución para el archivo CSV residual.

      Conclusión

      A lo largo de este tutorial , escribió un módulo de Node.js para administrar los elementos TODO y probó el código manualmente usando el REPL de Node.js. Luego, creó un archivo de prueba y usó el marco Mocha para ejecutar pruebas automatizadas. Con el módulo assert, pudo verificar que su código funciona. También probó funciones síncronas y asíncronas con Mocha. Finalmente, creó enlaces con Mocha que aportan legibilidad y una capacidad de mantenimiento muy superiores al escribir varios casos de prueba relacionados.

      Ahora que cuenta con este conocimiento, anímese a escribir pruebas para nuevos módulos de Node.js que cree. ¿Puede pensar en las entradas y los resultados de su función y escribir su prueba antes que su código?

      Si desea más información sobre el marco de prueba Mocha, consulte la documentación oficial de Mocha. Si desea seguir aprendiendo sobre Node.js, puede volver a la página de la serie Cómo desarrollar código en Node.js.



      Source link