Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 75 – API JavaScript, Node.js e Express – Mocking – jest.mock(), nock para HTTP

Imagem destacada da aula de API

Introdução (3 min)

Estimados alunos e futuros arquitetos de software, sejam bem-vindos à nossa Aula 75, onde desvendaremos um dos pilares da engenharia de software de alta performance: o Mocking. Imagine que você está desenvolvendo um novo modelo de carro esportivo. Para testar a segurança e o desempenho, você não usa o motor final, caríssimo e ainda em desenvolvimento. Em vez disso, você emprega um motor de teste, um protótipo ou até um simulador computadorizado que se comporta exatamente como o motor real, mas sem os riscos ou custos associados.

No universo das APIs modernas, este “motor de teste” é o que chamamos de mock. Ele nos permite simular o comportamento de partes complexas ou externas do nosso sistema, como bancos de dados, serviços de terceiros (pagamentos, e-mail, redes sociais) ou outras microservices. Isso é absolutamente essencial para construir APIs robustas, rápidas e confiáveis. Sem mocking, nossos testes seriam lentos, caros e, pior, inconsistentes, dependendo da disponibilidade e do estado de sistemas externos. É uma técnica vital para a agilidade e a estabilidade do desenvolvimento.

Nesta aula, vamos mergulhar fundo na prática. Você aprenderá a implementar mocks utilizando duas ferramentas poderosas no ecossistema Node.js/Express: jest.mock() para simular módulos internos e nock para interceptar e falsificar requisições HTTP externas. Nosso objetivo é construir um ambiente de teste onde sua API Express possa ser testada com total independência, simulando cenários complexos de interação com outros serviços.

Este conhecimento é um diferencial valioso para qualquer desenvolvedor Node.js que aspire a padrões de qualidade enterprise, garantindo que suas aplicações Express, mesmo rodando em um HostGator Plano M, sejam sempre testáveis e resilientes.

Conceito Fundamental (7 min)

O conceito de Mocking insere-se na categoria mais ampla dos Test Doubles. Em suma, um Test Double é qualquer objeto que substitui um objeto real para fins de teste. Existem vários tipos, e é fundamental compreendê-los para usar a terminologia correta na indústria:

    • Dummy Objects: São passados, mas nunca usados. Servem para preencher parâmetros obrigatórios.
    • Stubs: Fornecem respostas pré-determinadas a chamadas específicas durante o teste. Eles não registram as interações.
    • Spies: São stubs que também registram informações sobre como foram chamados (quantas vezes, com quais argumentos). Permitem verificar interações sem substituir o comportamento.
    • Fakes: Implementações leves com a mesma funcionalidade do real, mas com atalhos que os tornam inadequados para produção (ex: um banco de dados em memória).
    • Mocks: Objetos pré-programados com expectativas sobre como serão chamados. Eles validam se foram usados da maneira esperada durante o teste. Os mocks são capazes de substituir completamente um componente, simulando seu comportamento de forma controlada.

Nesta aula, focaremos nos Mocks por sua capacidade de isolar componentes e controlar o ambiente de teste.

jest.mock(): Substituição de Módulos

O jest.mock() é uma função global do Jest que possibilita substituir um módulo inteiro por uma implementação falsa. Isso é incrivelmente útil quando seu código tem uma dependência direta de um módulo local ou de um pacote npm. Por exemplo, se você tem um serviço que faz operações complexas e que não queremos testar em cada teste de unidade, o jest.mock() permite que você o substitua por uma versão simplificada, focando o teste no código que você realmente está testando.

Casos de uso reais:

    • Simular um módulo de banco de dados para evitar chamadas reais durante testes de unidade de um endpoint Express.
    • Falsificar um módulo de autenticação para testar rotas protegidas sem precisar de credenciais reais.
    • Substituir um módulo de logger para garantir que as mensagens certas são registradas.

nock: Interceptação de Requisições HTTP

O nock é uma biblioteca que intercepta requisições HTTP e HTTPS realizadas por Node.js, impedindo que elas atinjam a rede. Em vez disso, ele retorna respostas pré-configuradas. Ele atua em um nível de rede mais baixo, o que é ideal para testar interações com APIs externas.

Casos de uso reais:

    • Testar a integração com uma API de pagamento sem fazer transações monetárias reais.
    • Simular respostas de serviços externos (clima, cotação de moedas, APIs de terceiros) que sua aplicação consome.
    • Testar cenários de falha de rede ou erros específicos de uma API externa (timeouts, erros 500, etc.).

Vantagens da utilização de mocks:

    • Velocidade: Testes são executados muito mais rapidamente, pois não há latência de rede ou disco.
    • Determinismo: Os resultados dos testes são sempre os mesmos, independentemente do estado de sistemas externos.
    • Isolamento: Possibilita testar uma única unidade de código sem interferência de dependências.
    • Custo-benefício: Reduz custos associados a chamadas de APIs pagas ou uso excessivo de recursos de terceiros.
    • Resiliência: Os testes não falham se uma API externa estiver fora do ar.
    • Cenários Complexos: Facilita a simulação de cenários de erro ou condições muito específicas que seriam difíceis de reproduzir no ambiente real.

Desvantagens:

    • Fidelidade: Mocks podem divergir da realidade se a API externa mudar e o mock não for atualizado, levando a testes que passam, mas o código real falha em produção.
    • Manutenção: Os mocks precisam ser mantidos e atualizados à medida que as dependências evoluem.

A integração dessas ferramentas com Jest é fundamental. Jest fornece a estrutura de teste, enquanto jest.mock() e nock fornecem os meios para controlar as dependências, possibilitando testes de unidade e integração eficazes no seu ambiente Node.js.

Implementação Prática (10 min)

Vamos desenvolver um exemplo prático. Nossa API Express terá um serviço que busca informações de usuários de uma API externa (simulada). Então, vamos criar testes para garantir que tudo funcione, usando jest.mock() e nock.

Estrutura do Projeto


meu-app-mocking/
├── src/
│   ├── services/
│   │   └── userService.js
│   └── app.js
├── __tests__/
│   └── app.test.js
├── package.json
└── .env

1. Inicialização do Projeto e Instalação das Dependências

Primeiro, crie um novo projeto e instale as dependências. Para o HostGator Plano M, certifique-se de que o Node.js esteja na versão compatível (geralmente 12+), e que você possa instalar pacotes via npm ou yarn. As dependências são padrão.


mkdir meu-app-mocking
cd meu-app-mocking
npm init -y
npm install express axios dotenv winston
npm install --save-dev jest nock supertest

    • express: Framework web.
    • axios: Cliente HTTP para fazer requisições externas.
    • dotenv: Para carregar variáveis de ambiente.
    • winston: Para logging profissional.
    • jest: Framework de testes.
    • nock: Para mockar requisições HTTP.
    • supertest: Para testar rotas HTTP da nossa API Express.

2. Arquivo de Configuração de Ambiente (.env)

Crie um arquivo .env na raiz do projeto.


.env

📚 Informações da Aula

Curso: API Completo - Node.js & Express

Tempo estimado: 25 minutos

Pré-requisitos: JavaScript básico

EXTERNAL_API_BASE_URL=https://jsonplaceholder.typicode.com PORT=3000

3. Configuração do Logger (src/config/logger.js)

Para um logging profissional, utilizaremos o Winston.


// src/config/logger.js
const winston = require('winston');

// Define os formatos de log const logFormat = winston.format.printf(({ level, message, timestamp, stack }) => { return ${timestamp} ${level}: ${stack || message}; });

// Cria uma instância do logger const logger = winston.createLogger({ level: 'info', // Nível de log mínimo format: winston.format.combine( winston.format.colorize(), // Cores para o console winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), // Timestamp winston.format.errors({ stack: true }), // Inclui stack trace para erros logFormat // Formato personalizado ), transports: [ new winston.transports.Console(), // Log para o console // Adicionar outros transports, como arquivos, se necessário para produção // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], });

module.exports = logger;

4. Serviço de Usuário (src/services/userService.js)

Este serviço fará a requisição HTTP externa. Ele encapsula a lógica de busca.


// src/services/userService.js
const axios = require('axios');
const logger = require('../config/logger'); // Importa o logger
require('dotenv').config(); // Carrega as variáveis de ambiente

const EXTERNAL_API_BASE_URL = process.env.EXTERNAL_API_BASE_URL;

/* @class UserService @description Gerencia operações relacionadas a usuários externos. / class UserService { /* Busca todos os usuários da API externa. @returns {Promise} Uma promessa que resolve para um array de usuários. @throws {Error} Se a requisição à API externa falhar. / static async getAllUsers() { try { logger.info('Iniciando busca por todos os usuários na API externa.'); const response = await axios.get(${EXTERNAL_API_BASE_URL}/users); logger.info('Usuários obtidos com sucesso da API externa.'); return response.data; } catch (error) { logger.error(Erro ao obter usuários da API externa: ${error.message}, { stack: error.stack }); // Melhores práticas enterprise: Lançar um erro customizado para melhor tratamento throw new Error(Falha ao recuperar usuários: ${error.message}); } }

/ Busca um único usuário pelo ID da API externa. @param {number} userId - O ID do usuário a ser buscado. @returns {Promise} Uma promessa que resolve para um objeto de usuário. @throws {Error} Se o usuário não for encontrado ou a requisição à API externa falhar. / static async getUserById(userId) { if (!userId || typeof userId !== 'number' || userId <= 0) { logger.warn(Tentativa de buscar usuário com ID inválido: ${userId}); throw new Error('ID do usuário inválido.'); }

try { logger.info(Iniciando busca por usuário com ID: ${userId} na API externa.); const response = await axios.get(${EXTERNAL_API_BASE_URL}/users/${userId}); logger.info(Usuário com ID ${userId} obtido com sucesso.); return response.data; } catch (error) { if (error.response && error.response.status === 404) { logger.warn(Usuário com ID ${userId} não encontrado na API externa.); throw new Error(Usuário com ID ${userId} não encontrado.); } logger.error(Erro ao obter usuário ${userId} da API externa: ${error.message}, { stack: error.stack }); throw new Error(Falha ao recuperar usuário ${userId}: ${error.message}); } } }

module.exports = UserService;

5. Aplicação Express (src/app.js)

Nossa aplicação Express usará o UserService para expor uma rota.


// src/app.js
const express = require('express');
const UserService = require('./services/userService');
const logger = require('./config/logger'); // Importa o logger
require('dotenv').config(); // Carrega as variáveis de ambiente

const app = express(); app.use(express.json()); // Habilita o parsing de JSON no corpo das requisições

const PORT = process.env.PORT || 3000;

// Middleware simples para log de requisições app.use((req, res, next) => { logger.info(${req.method} ${req.originalUrl}); next(); });

/* Rota para obter todos os usuários. @route GET /users @returns {Array} Array de objetos de usuário. @throws {500} Erro interno do servidor. / app.get('/users', async (req, res) => { try { const users = await UserService.getAllUsers(); res.status(200).json(users); } catch (error) { logger.error(Erro na rota GET /users: ${error.message}, { stack: error.stack }); res.status(500).json({ message: 'Erro interno do servidor ao buscar usuários.', error: error.message }); } });

/* Rota para obter um usuário específico pelo ID. @route GET /users/:id @param {string} id - O ID do usuário. @returns {object} Objeto de usuário. @throws {400} Requisição inválida (ID não numérico). @throws {404} Usuário não encontrado. @throws {500} Erro interno do servidor. / app.get('/users/:id', async (req, res) => { const userId = parseInt(req.params.id, 10); // Validação de entrada robusta if (isNaN(userId) || userId <= 0) { logger.warn(Tentativa de acesso a /users com ID inválido: ${req.params.id}); return res.status(400).json({ message: 'ID de usuário inválido. Deve ser um número inteiro positivo.' }); }

try { const user = await UserService.getUserById(userId); res.status(200).json(user); } catch (error) { logger.error(Erro na rota GET /users/${userId}: ${error.message}, { stack: error.stack }); if (error.message.includes('não encontrado')) { return res.status(404).json({ message: error.message }); } res.status(500).json({ message: 'Erro interno do servidor ao buscar usuário.', error: error.message }); } });

// Iniciando o servidor if (process.env.NODE_ENV !== 'test') { // Previne o servidor de iniciar durante os testes app.listen(PORT, () => { logger.info(Servidor Express rodando na porta ${PORT}); }); }

module.exports = app; // Exporta o app para ser usado nos testes

6. Arquivo de Testes (__tests__/app.test.js)

Aqui, usaremos jest.mock() e nock para testar nossa API.


// __tests__/app.test.js
const request = require('supertest'); // Para testar rotas HTTP
const nock = require('nock'); // Para mockar requisições HTTP externas
const app = require('../src/app'); // Nossa aplicação Express
const logger = require('../src/config/logger'); // Importa o logger para suprimir logs em teste
const UserService = require('../src/services/userService'); // Nosso serviço

// Suprime logs durante os testes para evitar poluição no console jest.mock('../src/config/logger', () => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), }));

describe('API de Usuários', () => {

// Limpa todos os mocks HTTP do nock antes de cada teste // Isso é uma melhor prática crucial para evitar vazamentos de mocks entre testes beforeEach(() => { nock.cleanAll(); // Limpa todos os interceptores HTTP definidos pelo nock });

// Teste para a rota GET /users utilizando nock para a requisição externa test('GET /users deve retornar uma lista de usuários da API externa', async () => { const mockUsers = [ { id: 1, name: 'Usuário Mock 1' }, { id: 2, name: 'Usuário Mock 2' } ];

// Configura o nock para interceptar a requisição GET para /users // e retornar nossos dados mockados nock(process.env.EXTERNAL_API_BASE_URL) // O host da API externa .get('/users') // A rota que será interceptada .reply(200, mockUsers); // O que retornar (status HTTP e corpo da resposta)

// Faz a requisição HTTP para nossa API Express usando supertest const response = await request(app).get('/users');

// Verifica o status da resposta e os dados retornados expect(response.statusCode).toEqual(200); expect(response.body).toEqual(mockUsers);

// Opcional: Verificar se o logger foi chamado com informações relevantes expect(logger.info).toHaveBeenCalledWith('GET /users'); expect(logger.info).toHaveBeenCalledWith('Iniciando busca por todos os usuários na API externa.'); expect(logger.info).toHaveBeenCalledWith('Usuários obtidos com sucesso da API externa.'); });

// Teste para cenário de falha da API externa na rota GET /users test('GET /users deve retornar 500 se a API externa falhar', async () => { // Configura o nock para simular uma falha na requisição externa nock(process.env.EXTERNAL_API_BASE_URL) .get('/users') .reply(500, { message: 'Erro interno na API externa' }); // Simula um erro 500

const response = await request(app).get('/users');

expect(response.statusCode).toEqual(500); expect(response.body).toHaveProperty('message', 'Erro interno do servidor ao buscar usuários.'); expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Falha ao recuperar usuários: Request failed with status code 500'), expect.any(Object)); });

// Teste para a rota GET /users/:id utilizando nock test('GET /users/:id deve retornar um único usuário', async () => { const userId = 1; const mockUser = { id: userId, name: 'Usuário Mock ID 1' };

nock(process.env.EXTERNAL_API_BASE_URL) .get(/users/${userId}) .reply(200, mockUser);

const response = await request(app).get(/users/${userId});

expect(response.statusCode).toEqual(200); expect(response.body).toEqual(mockUser); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining(Iniciando busca por usuário com ID: ${userId} na API externa.)); });

// Teste para usuário não encontrado na rota GET /users/:id test('GET /users/:id deve retornar 404 se o usuário não for encontrado', async () => { const userId = 999; // ID que sabemos que não existe no mock

nock(process.env.EXTERNAL_API_BASE_URL) .get(/users/${userId}) .reply(404, {}); // Simula um erro 404

const response = await request(app).get(/users/${userId});

expect(response.statusCode).toEqual(404); expect(response.body).toHaveProperty('message', Usuário com ID ${userId} não encontrado.); expect(logger.warn).toHaveBeenCalledWith(Usuário com ID ${userId} não encontrado na API externa.); });

// Teste para validação de entrada inválida na rota GET /users/:id test('GET /users/:id deve retornar 400 para ID inválido', async () => { const response = await request(app).get('/users/abc');

expect(response.statusCode).toEqual(400); expect(response.body).toHaveProperty('message', 'ID de usuário inválido. Deve ser um número inteiro positivo.'); expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Tentativa de acesso a /users com ID inválido: abc')); });

// Exemplo de como usar jest.mock() para mockar o UserService diretamente // Isso seria útil para testes de unidade mais finos onde você não quer simular a rede describe('UserService mocking com jest.mock()', () => { // Limpa os mocks antes de cada teste neste bloco beforeEach(() => { // jest.clearAllMocks() limpa chamadas para mocks, mas não redefine as implementações // jest.resetAllMocks() redefine mocks para sua implementação original (útil se você fez uma mock implementation) // jest.restoreAllMocks() restaura mocks ao comportamento original (para spies) jest.clearAllMocks(); // Limpa o histórico de chamadas do logger mockado também });

test('UserService.getAllUsers deve retornar dados mockados com jest.mock()', async () => { const mockUsers = [{ id: 10, name: 'Mock Jest User' }]; // Usa jest.mock() para substituir o módulo UserService inteiro // A fábrica de mock retorna um objeto com uma função getAllUsers // que retorna os dados mockados. jest.mock('../src/services/userService', () => ({ // Retorna um objeto com a função estática getAllUsers mockada getAllUsers: jest.fn(() => Promise.resolve(mockUsers)), getUserById: jest.fn(), // Mocka também outras funções que podem ser chamadas }));

// Requer o serviço APÓS o jest.mock() para que ele pegue a versão mockada const MockedUserService = require('../src/services/userService');

// Chamamos diretamente a função do serviço (não via rota HTTP) const users = await MockedUserService.getAllUsers();

expect(users).toEqual(mockUsers); expect(MockedUserService.getAllUsers).toHaveBeenCalledTimes(1); // Verifica que o logger original (mockado) NÃO foi chamado com as mensagens de rede expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Iniciando busca por todos os usuários na API externa.')); });

test('UserService.getUserById deve lidar com erro de serviço mockado com jest.mock()', async () => { const errorMessage = 'Erro mockado ao buscar usuário'; jest.mock('../src/services/userService', () => ({ getAllUsers: jest.fn(), getUserById: jest.fn(() => Promise.reject(new Error(errorMessage))), }));

const MockedUserService = require('../src/services/userService');

await expect(MockedUserService.getUserById(1)).rejects.toThrow(errorMessage); expect(MockedUserService.getUserById).toHaveBeenCalledWith(1); }); }); });

7. Script de Teste no package.json

Adicione um script para rodar seus testes.


{
  "name": "meu-app-mocking",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node src/app.js",
    "test": "jest --detectOpenHandles --forceExit"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.21.1",
    "dotenv": "^10.0.0",
    "express": "^4.17.1",
    "winston": "^3.3.3"
  },
  "devDependencies": {
    "jest": "^27.0.6",
    "nock": "^13.1.1",
    "supertest": "^6.1.3"
  }
}

Para Rodar os Testes:


npm test

Se tudo estiver correto, você verá todos os testes passando. A opção --detectOpenHandles ajuda a identificar recursos que impedem o Jest de sair, e --forceExit força a saída, útil com nock e servidores Express que podem persistir.

Exercício Hands-On (5 min)

Agora é sua vez de aplicar esses conceitos. Vamos expandir nossa API para interagir com uma segunda API externa fictícia: um serviço de clima.

Desafio Prático: Implementar um Serviço de Clima e Mocká-lo

Objetivo: Adicionar uma nova rota à sua API Express que busca dados de clima para uma cidade específica de uma API externa (https://api.weatherapi.com/v1/current.json, por exemplo, embora não usaremos chaves reais nem faremos a requisição de fato). Você deverá então criar um novo arquivo de teste para este serviço, utilizando nock para simular a resposta da API de clima.

Passo a passo da implementação:

    • Atualizar .env: Adicione uma URL base para a API de clima e uma chave (fictícia, pois não faremos a chamada real).
      
      

      .env

      EXTERNAL_API_BASE_URL=https://jsonplaceholder.typicode.com WEATHER_API_BASE_URL=https://api.weatherapi.com/v1 WEATHER_API_KEY=YOUR_MOCKED_API_KEY PORT=3000

    • Criar um novo serviço de clima (src/services/weatherService.js):

      Desenvolva um serviço similar ao userService.js que tenha um método estático getCurrentWeather(city).

      
      // src/services/weatherService.js
      const axios = require('axios');
      const logger = require('../config/logger');
      require('dotenv').config();

      const WEATHER_API_BASE_URL = process.env.WEATHER_API_BASE_URL; const WEATHER_API_KEY = process.env.WEATHER_API_KEY;

      class WeatherService { static async getCurrentWeather(city) { if (!city || typeof city !== 'string' || city.trim() === '') { logger.warn('Tentativa de buscar clima com nome de cidade inválido.'); throw new Error('Nome da cidade inválido.'); }

      try { logger.info(Buscando clima atual para ${city}.); const response = await axios.get(${WEATHER_API_BASE_URL}/current.json, { params: { key: WEATHER_API_KEY, q: city } }); logger.info(Clima para ${city} obtido com sucesso.); return response.data; } catch (error) { logger.error(Erro ao obter clima para ${city}: ${error.message}, { stack: error.stack }); if (error.response && error.response.status === 400) { throw new Error(Cidade '${city}' não encontrada ou parâmetros inválidos.); } throw new Error(Falha ao recuperar clima para ${city}: ${error.message}); } } }

      module.exports = WeatherService;

    • Adicionar uma nova rota à sua aplicação Express (src/app.js):

      Crie uma rota /weather/:city que use o WeatherService.

      
      // src/app.js (apenas a nova rota, adicione ao arquivo existente)
      const WeatherService = require('./services/weatherService'); // Adicione este import

      // ... (código existente) ...

      / Rota para obter o clima atual de uma cidade. @route GET /weather/:city @param {string} city - O nome da cidade. @returns {object} Dados do clima. @throws {400} Requisição inválida (nome da cidade ausente). @throws {404} Cidade não encontrada. @throws {500} Erro interno do servidor. / app.get('/weather/:city', async (req, res) => { const city = req.params.city;

      if (!city) { logger.warn('Tentativa de acesso a /weather sem nome de cidade.'); return res.status(400).json({ message: 'Nome da cidade é obrigatório.' }); }

      try { const weatherData = await WeatherService.getCurrentWeather(city); res.status(200).json(weatherData); } catch (error) { logger.error(Erro na rota GET /weather/${city}: ${error.message}, { stack: error.stack }); if (error.message.includes('não encontrada') || error.message.includes('parâmetros inválidos')) { return res.status(404).json({ message: error.message }); } res.status(500).json({ message: 'Erro interno do servidor ao buscar clima.', error: error.message }); } });

      // ... (restante do código existente) ...

    • Criar um novo arquivo de teste (__tests__/weather.test.js):

      Crie testes para a nova rota, utilizando nock para mockar a API de clima e supertest para a rota Express.

      
      // __tests__/weather.test.js
      const request = require('supertest');
      const nock = require('nock');
      const app = require('../src/app');
      const logger = require('../src/config/logger'); // Para suprimir logs

      // Suprime logs durante os testes jest.mock('../src/config/logger', () => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), }));

      describe('API de Clima', () => { beforeEach(() => { nock.cleanAll(); // Limpa mocks do nock antes de cada teste });

      // Teste para sucesso na busca de clima test('GET /weather/:city deve retornar dados do clima com sucesso', async () => { const city = 'SaoPaulo'; const mockWeatherData = { location: { name: 'Sao Paulo', region: 'Sao Paulo' }, current: { temp_c: 25.5, condition: { text: 'Sunny' } } };

      // Mocka a requisição à API de clima nock(process.env.WEATHER_API_BASE_URL) .get('/current.json') .query({ key: process.env.WEATHER_API_KEY, q: city }) // Importante: incluir os parâmetros de query .reply(200, mockWeatherData);

      const response = await request(app).get(/weather/${city});

      expect(response.statusCode).toEqual(200); expect(response.body).toEqual(mockWeatherData); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining(Buscando clima atual para ${city}.)); });

      // Teste para cidade não encontrada test('GET /weather/:city deve retornar 404 se a cidade não for encontrada', async () => { const city = 'CidadeInexistente';

      // Mocka a falha da API de clima com status 400 (API de clima geralmente retorna 400 para cidade inválida) nock(process.env.WEATHER_API_BASE_URL) .get('/current.json') .query({ key: process.env.WEATHER_API_KEY, q: city }) .reply(400, { error: { code: 1006, message: 'No matching location found.' } });

      const response = await request(app).get(/weather/${city});

      expect(response.statusCode).toEqual(404); expect(response.body).toHaveProperty('message', Cidade '${city}' não encontrada ou parâmetros inválidos.); expect(logger.error).toHaveBeenCalledWith(expect.stringContaining(Falha ao recuperar clima para ${city}: Request failed with status code 400), expect.any(Object)); });

      // Teste para erro interno da API de clima test('GET /weather/:city deve retornar 500 se a API de clima falhar internamente', async () => { const city = 'Rio';

      nock(process.env.WEATHER_API_BASE_URL) .get('/current.json') .query({ key: process.env.WEATHER_API_KEY, q: city }) .reply(500, { message: 'Internal server error from weather API' });

      const response = await request(app).get(/weather/${city});

      expect(response.statusCode).toEqual(500); expect(response.body).toHaveProperty('message', 'Erro interno do servidor ao buscar clima.'); expect(logger.error).toHaveBeenCalledWith(expect.stringContaining(Falha ao recuperar clima para ${city}: Request failed with status code 500), expect.any(Object)); });

      // Teste para entrada inválida test('GET /weather/:city deve retornar 400 se a cidade for uma string vazia', async () => { const response = await request(app).get('/weather/ '); // Espaço em branco

      expect(response.statusCode).toEqual(400); expect(response.body).toHaveProperty('message', 'Nome da cidade inválido.'); // Mensagem do service expect(logger.warn).toHaveBeenCalledWith('Tentativa de buscar clima com nome de cidade inválido.'); }); });

    • Atualizar package.json para incluir o novo teste:

      Ajuste o script de teste para rodar ambos os conjuntos de testes (opcional, Jest encontra automaticamente arquivos .test.js).

      
      // ...
      "scripts": {
          "start": "node src/app.js",
          "test": "jest --detectOpenHandles --forceExit __tests__/*.test.js"
      },
      // ...
      

Como testar e validar o resultado:

Após implementar o código, salve todos os arquivos e execute no terminal:


npm test

Todos os testes (dos usuários e do clima) devem passar com sucesso. Você estará validando não apenas a funcionalidade da sua API, mas também a sua capacidade de isolar e testar interações com serviços externos de forma controlada.

Troubleshooting dos erros mais comuns:

    • Nock: No match for GET ...: Este é o erro mais frequente. Significa que o nock não conseguiu encontrar uma requisição correspondente para interceptar. Verifique:

      • URL base está correta? (nock(process.env.WEATHER_API_BASE_URL))
      • Método HTTP está correto? (.get(...))
      • Caminho da URL está correto? (.get('/current.json'))
      • Parâmetros de query estão corretos? (.query({ key: ..., q: ... }))
      • Headers estão corretos (se aplicável)?
    • Testes falhando por dependência de rede: Se você esqueceu de configurar o nock para uma requisição externa, o axios fará uma chamada real, que pode falhar ou retornar dados inesperados. Garanta que todas as chamadas HTTP externas estão devidamente mockadas em seus testes.
    • Testes de mocks vazando entre si: É por isso que nock.cleanAll() em beforeEach é fundamental. Sem ele, um mock definido em um teste pode afetar o próximo teste, levando a resultados inconsistentes.
    • Problemas com jest.mock(): Certifique-se de que o caminho para o módulo está correto e que o módulo mockado está sendo importado após a chamada a jest.mock() em um arquivo de teste separado para garantir que a versão mockada seja usada.

Próximos passos sugeridos:

    • Explore mais a fundo o Nock API para simular delays, erros de rede, múltiplos interceptores para a mesma URL, ou diferentes respostas baseadas em dados do corpo da requisição.
    • Investigue jest.spyOn(), que permite observar chamadas a métodos de um objeto sem alterar sua implementação original, sendo ideal para verificar se uma função foi chamada.
    • Aprenda sobre Testes de Integração e Testes End-to-End (E2E) para entender onde mocks se encaixam e onde testes com dependências reais são mais apropriados.
    • Considere a integração de seus testes com um sistema de Integração Contínua (CI) como GitHub Actions ou GitLab CI para que seus testes sejam executados automaticamente a cada mudança de código.

Parabéns! Você acaba de dar um passo gigantesco em direção à construção de APIs de alta qualidade, preparadas para qualquer desafio no mundo real, com a flexibilidade e robustez que apenas o mocking pode proporcionar.

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas