Criando APIs testáveis com Node.js (Design Patterns)
Introdução:
Neste tutorial, vamos aprender como criar APIs testáveis usando Node.js, focando em design patterns que nos ajudam a escrever código limpo e modular. Testabilidade é uma característica muito importante em desenvolvimento de software, pois nos permite garantir que nosso código funcione corretamente e que possamos fazer alterações sem medo de quebrar outras partes do sistema.
Vamos usar o Node.js como nossa plataforma de desenvolvimento, pois ele é uma excelente escolha para a construção de APIs devido à sua flexibilidade, velocidade e fácil integração com bancos de dados e sistemas externos.
Design Patterns:
Antes de começarmos a programar, é importante entender alguns design patterns que são úteis ao construir APIs testáveis em Node.js.
-
MVC (Model-View-Controller): Este é um padrão de arquitetura muito comum em desenvolvimento web. Ele divide a aplicação em três partes distintas: o Model (modelo), que representa os dados da aplicação; o View (visão), que é a interface com o usuário; e o Controller (controlador), que coordena a interação entre o modelo e a visão. Este padrão ajuda a separar as preocupações e torna o código mais organizado e fácil de manter.
-
Dependency Injection: Este padrão nos permite injetar as dependências de uma classe em vez de criá-las dentro da própria classe. Isso facilita a escrita de testes, pois podemos substituir facilmente as dependências reais por mocks ou stubs durante os testes.
- Factory Pattern: Este padrão nos permite instanciar objetos sem precisar conhecer os detalhes de como eles são criados. Isso é útil quando queremos desacoplar a criação de objetos e fornecer uma interface mais flexível para instanciá-los.
Estrutura do projeto:
Vamos começar criando a estrutura básica do nosso projeto. Para simplificar, vamos dividir a aplicação em três pastas principais: models (para os modelos de dados), controllers (para os controladores da API) e routes (para as rotas da API). Além disso, criaremos uma pasta tests para os testes unitários da aplicação.
Dentro do projeto, teremos os seguintes arquivos:
- app.js: arquivo principal da aplicação que inicializa o servidor e configura as rotas
- models/: pasta para os modelos de dados
- controllers/: pasta para os controladores da API
- routes/: pasta para definir as rotas da API
- tests/: pasta para os testes unitários
Construindo a API:
Vamos agora começar a construir a nossa API utilizando os design patterns mencionados anteriormente.
- Model:
Vamos criar um modelo de dados simples para representar um usuário. Para isso, crie um arquivo user.model.js dentro da pasta models com o seguinte conteúdo:
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
module.exports = UserModel;
- Controller:
Agora vamos criar um controlador para lidar com as requisições relacionadas aos usuários. Para isso, crie um arquivo user.controller.js dentro da pasta controllers com o seguinte conteúdo:
const UserModel = require('../models/user.model');
class UserController {
constructor(userModel) {
this.userModel = userModel;
}
async createUser(name, email) {
const newUser = new this.userModel(name, email);
// salvar usuário no banco de dados
return newUser;
}
}
module.exports = UserController;
- Routes:
Agora vamos definir as rotas da nossa API. Para isso, crie um arquivo user.routes.js dentro da pasta routes com o seguinte conteúdo:
const express = require('express');
const UserController = require('../controllers/user.controller');
const router = express.Router();
const userController = new UserController();
router.post('/user', async (req, res) => {
const { name, email } = req.body;
const newUser = await userController.createUser(name, email);
res.json(newUser);
});
module.exports = router;
- App.js:
Por último, vamos configurar as rotas da nossa API no arquivo app.js:
const express = require('express');
const userRoutes = require('./routes/user.routes');
const app = express();
app.use(express.json());
app.use(userRoutes);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Testando a API:
Agora que já construímos a estrutura básica da nossa API, vamos escrever alguns testes unitários para garantir que tudo esteja funcionando corretamente. Para isso, crie um arquivo user.controller.test.js dentro da pasta tests com o seguinte conteúdo:
const UserController = require('../controllers/user.controller');
describe('UserController', () => {
let userController;
beforeEach(() => {
userController = new UserController();
});
test('createUser should return a new user object', async () => {
const newUser = await userController.createUser('John Doe', 'john@example.com');
expect(newUser.name).toBe('John Doe');
expect(newUser.email).toBe('john@example.com');
});
});
Agora podemos rodar os testes usando um runner de testes como o Jest para confirmar que nossa API está funcionando corretamente.
Conclusão:
Neste tutorial, aprendemos como criar APIs testáveis com Node.js usando design patterns como MVC, Dependency Injection e Factory Pattern. Estes padrões nos ajudam a escrever código limpo e modular que é fácil de testar e manter. Espero que você tenha achado este tutorial útil e esteja pronto para aplicar estes conceitos em seus próprios projetos. Boa sorte e feliz codificação!
Qual a vantagem de se usar funcao nos useCases e controllers? Classes nao seria melhor? Achei interessante o uso de funcoes
QUERO ESSA CAMISA!
seria melhor que fosse usado interface e injeção de dependência, isso é costume ser feito em c#, causa o mesmo efeito de desacoplamento no final
Deixei pra assistir a live depois e cortaram a explicação toda… ¬¬
Uma questão mais filosófica do que prática que costumo discutir com colegas é se testes de regra de negócio (testes de use cases) devem checar estado de repositório em memória (como o Diego mostra no tempo 1:00:50). Particularmente defendo que não devem, porque na prática isso testa a implementação do repositório em memória ao invés de testar a regra de negócio em si, acredito que assim como o use case não deve conhecer a implementação do repositório, o teste do use case também não deve conhecer o estado/implementação do repositório em memória, pois o que faz parte da regra de negócio é a chamada do método do repositório (isso sim deve ser testado e deve ser o suficiente). Quando se testa o estado do repositório em memória, a partir de um teste de use case, está se testando um side effect de uma classe que não importa para a regra de negócio, já que estamos usando inversão de controle e injeção de dependência.
Que comando http eh esse q vc usa no terminal para fazer requisicoes http?
Injeção de Dependência e Inversão de Controle!
Qual é o modelo do seu fone Diegão?
Alquém pra me ajudar,
não entendi como o diego usou o arquivo setup-db.js
Vendo agora, em 11:53 quando o Diegão fala que o projeto vai estar hoje no Ignite, a vontade que tive foi de sair "correndo" pra uma nova aba e ver o projeto lá… mas eu vou terminar de asssitir esse video aqui que tá bom demais! Como sempre, conteúdo inigualável!
Olá, eu sou novo na programação mas queria tirar uma duvida muito boba, no caso se a tabela que eu utilizo no banco de dados tenha 30 campos, para fazer os testes eu teria que ficar replicando esses 30 campos ? mesmo que seja criando um modelo da tabela em outro arquivo.
Mais uma grande aula, Diegão. 🚀
Só uma pequena correção: o "design pattern" que você mencionou não é "inversão de dependência" e sim "injeção de dependência". 🙂
Se o Serasa tivesse a simpatia que todos da Rocket Seat tem eu não teria dívidas
Diego quando vc diz que os testes melhoram a arquitetura eu entendo que se você está misturando conceitos, no caso design e arquitetura. O design de software está mais relacionado com a implementação específica de um software, enquanto a arquitetura de software está mais relacionada com a estrutura geral do sistema de software. A arquitetura de software é uma atividade mais abrangente e estratégica que fornece a estrutura geral para o desenvolvimento do software, desde a escolha de banco de dados, sistemas de fila e etc. Não tem como guiar escolhas de arquitetura por meio de testes, já design de um sistema ou módulo podem sim emergir dos testes.
Desculpe, só para complementar, a distância ente uma conexão real e uma mokada é qiilometros e quilometros de distância, um abraço.
Diego, simular uma conexao com o banco, indo desde a conexão, passando pela inserção de dados e o retorno para o usuário da mensagem de confirmação, não "PODEM" ser mokados, devem ser feitos de maneira real…, um abraço.
Qual o nome desse tema do vscode?
Alguém tem o link do formulário ?