Testes Unitários com Mocha e Chai
Olá leitor, seja bem vindo a mais uma lição da jornada do desenvolvedor NodeJS 😎
Hoje nos iremos aprender um assunto bastante pertinente, e muita das vezes esquecidos, ou até mesmo ignorados, no mundo do desenvolvimento de software.
Estou me referindo a nada mais, nada menos do que os TESTES UNITÁRIOS!
E a proposito, você já ouviu falar deles? 🧐
O que é um teste unitário?
Um teste unitário, ou também conhecidos como testes unitários (pois podem haver mais de um), é uma prática de desenvolvimento de software, onde testamos cada parte individual da nossa aplicação, a fim de verificar se tudo está funcionando conforme o esperado.
Cada parte individual, são conhecidas como unidades, que por sua vez, podem ser desde funções ou classes do Javascript.
Quando implementamos um teste unitário em nossa aplicação, o nosso principal objetivo é isolar cada lógica do código de modo a verificar se ela produz os resultados esperados.
Além disso, esses testes facilitam pra caramba a vida do desenvolvedor, e também na identificação de erros que podem ocorrer durante a execução da sua aplicação.
Agora imagina você testando manualmente cada aspecto do seu código para ver se está tudo certo? Imagina o trabalhão que isso iria dar 😱
Antigamente (e até hoje), nós adiciovamos novas funcionalidades ou correções atreladas a nossa aplicação, e após isso, testávamos para ver se ela funciona corretamente (bem, pelo menos é isso que os stakeholders esperam, que você teste a sua aplicação antes de subir para a produção).
Entretanto, a expressão "Mas na minha máquina funciona", vem se tornando cada vez mais presente no cotidiano dos desenvolvedores. E foi pensando em solucionar este problema, que os testes unitários também foram criados!
Pois com eles, antes da sua aplicação subir para qualquer outro ambiente (desenvolvimento, homologação e produção), bibliotecas estariam testando cada aspecto da sua aplicação, de modo a garantir que tudo esteja funcionando corretamente em todos os ambientes.
E se alguma coisa der errado durante os testes, a aplicação não subirá para o ambiente em questão (ou pelo menos não deveria subir).
Basicamente, um teste unitário está relacionado ao uso de uma biblioteca (ou aplicação) capaz de se comunicar diretamente com nossos arquivos (funções, classes, views...), para posteriormente simular uma operação ou analisar o comportamento dos mesmo, verificando se eles retornaram as respostas esperadas.
Testes unitários e sua relação com o NodeJS
No contexto de aplicações feitas com NodeJS, os testes unitários são fundamentais para garantir que cada parte do código funcione como esperado de forma isolada.
Sendo assim, nós podemos aplicar testes unitários em diferentes áreas do nosso código, como por exemplo:
Funções e Lógicas de Negócio: cada função pode ser testada isoladamente para garantir que está retornando os resultados corretos com base nas entradas fornecidas.
Middlewares: se a aplicação utiliza middlewares, como em um servidor Express, eles podem ser testados para verificar como processam requisições e respostas, garantindo que os fluxos lógicos no pipeline de requisição estejam corretos.
APIs e Endpoints: para servidores RESTful, os endpoints podem ser testados para verificar como respondem a diferentes requisições, simulando interações com o cliente e verificando se as respostas HTTP
, códigos de status e dados retornados estão corretos.
Integração com Banco de Dados: em vez de testar diretamente o banco de dados, é possível mockar (simular) interações com o banco de dados para testar como a nossa aplicação reage a dados retornados, evitando uma dependência direta de um banco real durante os testes.
Manipulação de Arquivos: são funções que leem ou escrevem em arquivos, ao mesmo tempo que podem ser testadas para garantir que estão funcionando corretamente com diferentes tipos de dados, tamanhos de arquivos ou erros potenciais.
Eventos Assíncronos e Promises: estes testes podem garantir que as funções assíncronas funcionem corretamente, retornando os resultados esperados ou tratando erros de forma adequada.
Módulos Independentes: em uma aplicação modular, cada módulo pode ser testado separadamente para verificar se a comunicação entre eles ocorre de maneira correta.
O que é o Mocha?
O Mocha é um framework de teste JavaScript usado principalmente para executar testes em ambientes de NodeJS.
Com ele você será capaz de criar, organizar e executar testes de maneira simples e flexível, sendo considerado uma das ferramentas mais populares para testes no ecossistema do NodeJS.
No caso dele, nos também trabalhamos a biblioteca Chai, que realiza asserções durante os nossos testes 😉
Dito isso, vamos clocar a mão na massa, e realizar nossos testes com NodeJS!
Criando nosso projeto de testes
Para que possamos realizar testes unitários em nossa aplicação, é obvio que precisamos de uma aplicação pré-pronta, não é verdade? Pois não tem como testar alguma coisa em algo que ainda nem foi feito rs
Sendo assim, vamos começar criando uma pasta do nosso projeto, no meu caso, eu criei uma pasta chamada de Testes dentro da minha área de trabalho (desktop):
Com o seu terminal (Prompt de Comando) aberto na pasta raiz que acabamos de criar, precisamos inicializar o nosso projeto por meio do NPM
, sendo assim, execute o seguinte comando abaixo:
npm init -y
A flag -y
, como você já deve saber, cria um novo projeto de forma enxuta, respondendo SIM para tudo 😅
Em seguida, vamos instalar a biblioteca do express, pois iremos precisar dela para criar nossa aplicação:
npm install express
Além disso, vamos instalar mais duas outras bibliotecas, que são DotEnv, Handlebars, Sequelize e MySQL:
npm install sequelize mysql2 dotenv express-handlebars body-parser axios
Feito isso, não se esqueça de criar seu index.js
com uma mensagem bem legal de boas vindas:
console.log('Olá Mundo!');
Implementando nosso projeto de testes
Agora que já temos a base do projeto criada, vamos desenvolver uma aplicação bem simples que vai conter duas rotas principais:
/
será a nossa rota principal (GET
)/save
será a rota que vai receber alguns dados do formulário (POST
)
Dentro da nossa Página Inicial (/
), nós teremos uma view responsável por mostrar duas listas de nomes, a primeira é referente aos nomes vindos de uma API, e a segunda é uma lista de nomes advindos do nosso banco de dados (tabela usuarios).
Além disso, no final da página, teremos um formulário, onde o usuário poderá inserir um nome para ser salvo no banco de dados.
Já a rota /save
, por se tratar de uma rota de recebimento de dados, ela ficará responsável por inserir o nome no banco de dados, e conter um link que vai levar nosso usuário de volta a nossa página principal.
Com isso em mente, vamos contruir a nossa aplicação 🤓
Estrutura de pastas do nosso projeto:
/Testes
│
├── /db
│ └── connection.js
│
├── /models
│ └── Usuario.js
│
├── /controllers
│ ├── homeController.js
│ └── saveController.js
│
├── /routes
│ ├── homeRoutes.js
│ └── saveRoutes.js
│
├── /views
│ └── index.handlebars
│
├── /public
│ └── styles.css
│
├── .env
├── index.js
└── package.json
db > connection.js
:
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASS,
{
host: process.env.DB_HOST,
dialect: 'mysql'
}
);
module.exports = sequelize;
models > Usuario.js
:
const { DataTypes } = require('sequelize');
const sequelize = require('../db/connection');
const Usuario = sequelize.define('Usuario', {
nome: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'usuarios',
timestamps: false
});
module.exports = Usuario;
controllers > homeController.js
:
const axios = require('axios');
const Usuario = require('../models/Usuario');
class HomeController {
async home(req, res) {
try {
// Chamando a API JSONPlaceholder para buscar usuários
const apiResponse = await axios.get('https://jsonplaceholder.typicode.com/users');
// Extraindo os nomes dos usuários da API
const nomesApi = apiResponse.data.map(user => user.name);
// Buscando nomes do banco de dados
const usuariosDb = await Usuario.findAll();
// Extraindo apenas os nomes do banco de dados
const nomesDb = usuariosDb.map(usuario => usuario.dataValues.nome);
res.render('index', { nomesApi, nomesDb });
} catch (error) {
console.error('Erro ao carregar dados da API:', error);
res.status(500).send('Erro ao carregar dados');
}
}
}
module.exports = new HomeController();
controllers > saveController.js
:
const Usuario = require('../models/Usuario');
class SaveController {
async save(req, res) {
const { nome } = req.body;
if (!nome) {
return res.redirect('/');
}
try {
await Usuario.create({ nome });
res.redirect('/');
} catch (error) {
res.status(500).send('Erro ao salvar nome');
}
}
}
module.exports = new SaveController();
routes > homeRoutes.js
:
const express = require('express');
const router = express.Router();
const HomeController = require('../controllers/homeController');
router.get('/', HomeController.home);
module.exports = router;
routes > saveRoutes.js
:
const express = require('express');
const router = express.Router();
const saveController = require('../controllers/saveController');
router.post('/save', saveController.save);
module.exports = router;
views > index.handlebars
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lista de Nomes</title>
<link rel="stylesheet" href="/public/styles.css">
</head>
<body>
<h1>Lista de Nomes da API</h1>
<ul>
{{#each nomesApi}}
<li>{{this}}</li>
{{/each}}
</ul>
<h1>Lista de Nomes do Banco de Dados</h1>
<ul>
{{#each nomesDb}}
<li>{{this}}</li>
{{/each}}
</ul>
<h2>Adicionar Nome</h2>
<form action="/save" method="POST">
<input type="text" name="nome" placeholder="Digite o nome" required>
<button type="submit">Salvar</button>
</form>
</body>
</html>
public > style.css
:
body {
font-family: Arial, sans-serif;
}
.env
:
DB_NAME=nodejs
DB_USER=root
DB_PASS=SUASENHAAQUI
DB_HOST=localhost
PORT=3000
index.js
:
const express = require('express');
const bodyParser = require('body-parser');
const exphbs = require('express-handlebars');
const path = require('path');
require('dotenv').config();
const app = express();
// Conectar ao banco de dados
const sequelize = require('./db/connection');
sequelize.sync().then(() => console.log('Banco conectado'));
// Configurar body-parser
app.use(bodyParser.urlencoded({ extended: true }));
// Configurar handlebars como engine de views
app.engine('handlebars', exphbs.engine({
defaultLayout: false
}));
app.set('view engine', 'handlebars');
// Configurar pasta pública
app.use('/public', express.static(path.join(__dirname, 'public')));
// Rotas
app.use('/', require('./routes/homeRoutes'));
app.use('/', require('./routes/saveRoutes'));
// Iniciar o servidor
const PORT = process.env.PORT || 3000;
sequelize.sync({ force: false }) // force: false evita que as tabelas sejam recriadas
.then(() => {
console.log('Banco de dados sincronizado');
app.listen(PORT, () => {
console.log(`Servidor rodando na porta ${PORT}`);
});
})
.catch(err => {
console.error('Erro ao sincronizar o banco de dados:', err);
});
module.exports = app;// Precisamos exportar o app, pois iremos usá-lo no Mocha
Observação: note que no final do arquivo index.js
, nós estamos exportando ele em um módulo, precisamos disso pois o mocha
o usará como ponto de partida para nossos testes.
Além disso, não se esqueça de rodar o seu projeto node ./index.js
😉
Veja como ficou o resultado final:
Instalando a biblioteca Mocha e Chai
Para iniciarmos nossos testes, vamos abrir o terminal (Prompt de Comando) dentro da pasta raiz do seu projeto, e executar o comando abaixo:
npm install mocha chai@4.4.0 chai-http@4.4.0
A biblioteca chai-http
te permite simular requisições HTTP
. Observe que estamos instalando a biblioteca na versão 4.4.0
(em vez da 5.x), por conta de um problema de importação referente ao erro: chai.request is not a function.
Após isso, vamos criar uma nova pasta dentro do nosso projeto chamada de test:
Dentro dela ficarão todos os testes que iremos realizar com ambas as bibliotecas 😉
Criando nosso arquivos de testes
Dentro da pasta test que acabamos de criar, vamos criar 4 novos arquivos:
controller.test.js
route.test.js
database.test.js
view.test.js
Para cada arquivo de testes que você for criar no futuro, é uma boa prática especificar quais arquivos serão testados, e sempre nomeá-los com .test.js
, pois dessa forma o Mocha
vai interpretá-los de maneira automática.
Testando a nossa view
Dentro do arquivo view.test.js
, vamos implementar o nosso primeiro teste:
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../index'); // Ajuste o caminho se necessário
const { expect } = chai;
chai.use(chaiHttp);
describe('Teste da View HTML', () => {
it('Deve retornar o conteúdo correto da view index', (done) => {
chai.request(app)
.get('/')
.end((err, res) => {
expect(res).to.have.status(200);
//Devemos verificar se existe uma tag do tipo <h1>Lista de Nomes da API</h1>
expect(res.text).to.include('<h1>Lista de Nomes da API</h1>');
//Devemos verificar se existe uma tag do tipo <h1>Lista de Nomes do Banco de Dados</h1>
expect(res.text).to.include('<h1>Lista de Nomes do Banco de Dados</h1>');
done();
});
});
});
Vamos às explicações 😉
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../index'); // Ajuste o caminho se necessário
const { expect } = chai;
chai
: é uma biblioteca de asserções (assertions) que permite escrever testes com expectativas como .to.have.status(200)
ou .to.include(...)
.
chaiHttp
: um plugin do Chai
para testar requisições HTTP
(GET, POST, etc.).
app
: é o nosso objeto da aplicação Express que você está testando (que foi exportado dentro do index.js).
{ expect }
: a função expect
permite que você defina asserções que irão verificar o comportamento esperado da aplicação.
chai.use(chaiHttp);
O comando acima, registra o plugin chai-http
dentro do Chai
, permitindo que ele realize requisições HTTP
no teste.
describe('Teste da View HTML', () => {
describe
: aqui estamos definindo um conjunto de testes, que está nomeado como "Teste da View HTML". Isso ajuda a organizar e descrever os testes que você vai rodar.
it('Deve retornar o conteúdo correto da view index', (done) => {
it
: define um caso de teste individual. O nome do teste é "Deve retornar o conteúdo correto da view index".
O argumento done
é uma função de callback que indica quando o teste foi finalizado, que é bastante usado principalmente em testes assíncronos como este.
chai.request(app)
.get('/')
.end((err, res) => {
chai.request(app).get('/')
: faz uma requisição GET
para a rota /
da aplicação (ou seja, para o endpoint da página principal).
end((err, res) => {...})
: é uma função de callback que é chamada quando a requisição termina. O argumento err captura erros, e res contém a resposta da requisição.
expect(res).to.have.status(200);
Essa linha verifica se a resposta HTTP
tem o status 200
(OK), indicando que a requisição foi bem-sucedida.
expect(res.text).to.include('<h1>Lista de Nomes da API</h1>');
expect(res.text).to.include('<h1>Lista de Nomes do Banco de Dados</h1>');
expect(res.text)
: aqui estamos verificando o conteúdo do corpo da resposta (em HTML).
.to.include('<h1>...')
: aqui estamos verifica se o HTML
retornado inclui as tags <h1>
especificadas, garantindo que a view está renderizando esses elementos corretamente.
done();
O comando acima, indica que o teste terminou, dizendo ao Mocha que ele pode prosseguir para o próximo teste.
Para rodar este teste específico, basta executar no seu terminal (Prompt de Comando) o seguinte comando abaixo:
npx mocha ./test/view.test.js
O resultado final aparecerá no console
, indicando que o teste foi um sucesso:
Testando o nosso banco de dados
Já dentro do arquivo database.test.js
, vamos inserir a seguinte lógica:
const chai = require('chai');
const { expect } = chai;
const Usuario = require('../models/Usuario');
describe('Teste do Banco de Dados', () => {
before(async () => {
await Usuario.sync({ force: true }); // Limpa e cria a tabela
});
it('Deve inserir um usuário no banco de dados', async () => {
const usuario = await Usuario.create({ nome: 'Teste DB' });
expect(usuario).to.have.property('id');
expect(usuario).to.have.property('nome', 'Teste DB');
const usuarios = await Usuario.findAll();
expect(usuarios).to.have.lengthOf(1);
expect(usuarios[0].dataValues.nome).to.equal('Teste DB');
});
it('Deve buscar todos os usuários no banco de dados', async () => {
const usuarios = await Usuario.findAll();
expect(usuarios).to.be.an('array').that.is.not.empty;
});
});
Ele segue o mesmo esquema do código que vimos anteriormente, neste caso, vamos focar nas novas funcionalidades 🙃
before(async () => {
await Usuario.sync({ force: true }); // Limpa e cria a tabela
});
O before
é um hook do Mocha
que roda antes de todos os testes.
await Usuario.sync({ force: true })
: sincroniza o modelo Usuario
com o banco de dados. A opção force: true
faz com que a tabela seja recriada (excluindo dados anteriores), útil para garantir que os testes comecem com uma tabela limpa.
it('Deve inserir um usuário no banco de dados', async () => {
const usuario = await Usuario.create({ nome: 'Teste DB' });
expect(usuario).to.have.property('id');
expect(usuario).to.have.property('nome', 'Teste DB');
});
Usuario.create({ nome: 'Teste DB' }):
é criado um novo registro no banco de dados com o nome 'Teste DB'.
expect(usuario).to.have.property('id')
: verifica se o objeto retornado possui uma propriedade id
, o que indica que o usuário foi inserido com sucesso.
expect(usuario).to.have.property('nome', 'Teste DB')
: verifica se a propriedade nome
no objeto usuario contém o valor 'Teste DB'.
const usuarios = await Usuario.findAll();
expect(usuarios).to.have.lengthOf(1);
expect(usuarios[0].dataValues.nome).to.equal('Teste DB');
Usuario.findAll()
: faz uma consulta para obter todos os registros da tabela Usuario
.
expect(usuarios).to.have.lengthOf(1)
: verifica se existe exatamente um registro no banco, o que confirma que o usuário foi inserido corretamente.
expect(usuarios[0].dataValues.nome).to.equal('Teste DB')
: verifica se o nome do primeiro (e único) usuário na tabela é 'Teste DB'.
it('Deve buscar todos os usuários no banco de dados', async () => {
const usuarios = await Usuario.findAll();
expect(usuarios).to.be.an('array').that.is.not.empty;
});
No segundo teste que é realizado dentro desse arquivo, nos estamos verificando se a busca de todos os usuário esta funcionando corretamente.
Para rodar este teste específico, basta executar no seu terminal (Prompt de Comando) o seguinte comando abaixo:
npx mocha ./test/database.test.js
O resultado final aparecerá no console
, indicando que o teste foi um sucesso:
Testando nossas rotas
Dentro do arquivo route.test.js
, vamos inserir a seguinte lógica:
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../index'); // Ajuste o caminho se necessário
const { expect } = chai;
chai.use(chaiHttp);
describe('Testes das Rotas', () => {
it('Deve acessar a rota principal (GET /)', (done) => {
chai.request(app)
.get('/')
.end((err, res) => {
expect(res).to.have.status(200);
done();
});
});
it('Deve salvar um nome no banco de dados (POST /save)', (done) => {
chai.request(app)
.post('/save')
.send({ nome: 'Teste' })
.end((err, res) => {
expect(res).to.have.status(200); // Redireciona de volta
done();
});
});
});
A única diferença das lógicas dos testes anteriores, é que estamos simulando requisições do tipo get
e post
, e enviando dados por meio do send()
.
Para rodar este teste específico, basta executar no seu terminal (Prompt de Comando) o seguinte comando abaixo:
npx mocha ./test/route.test.js
O resultado final aparecerá no console
, indicando que o teste foi um sucesso:
Testando nossos controllers
Por fim, que tal testarmos nossos controladores? Para isso você precisa inserir a lógica abaixo dentro do arquivo controller.test.js
:
// test/controller.test.js
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../index'); // Ajuste o caminho se necessário
const HomeController = require('../controllers/homeController');
const { expect } = chai;
chai.use(chaiHttp);
describe('Teste do HomeController', () => {
it('Deve buscar nomes da API e do banco de dados', async () => {
const req = {};
const res = {
render: (view, data) => {
expect(data).to.have.property('nomesApi');
expect(data).to.have.property('nomesDb');
}
};
await HomeController.home(req, res);
});
});
req
: simula o objeto de requisição (request) que normalmente seria passado para o controller em um ambiente real (como o Express). Neste caso, ele está vazio porque o teste não depende de parâmetros específicos de requisição.
res
: simula o objeto de resposta (response), que normalmente é usado pelo controller para enviar a resposta ao cliente (ou para renderizar uma view).
render(view, data)
: o método render
é simulado, já que é assim que as views são normalmente renderizadas no Express (ex.: res.render('nomeDaView', dados)).
expect(data).to.have.property('nomesApi')
: verifica se o objeto data
, que seria passado para a view, contém a propriedade nomesApi
(dados vindos de uma API).
expect(data).to.have.property('nomesDb')
: verifica se o objeto data também contém a propriedade nomesDb
(dados vindos do banco de dados).
Para rodar este teste específico, basta executar no seu terminal (Prompt de Comando) o seguinte comando abaixo:
npx mocha ./test/controller.test.js
O resultado final aparecerá no console
, indicando que o teste foi um sucesso:
Rodando todos os testes de uma única vez
Para rodar todos os testes de uma única vez, basta executar o seguinte comando no seu terminal (Prompt de Comando):
npx mocha
Ou caso preferir, você pode chamar o mocha
por meio do npm test
, para isso, vamos precisar adicionar o seguinte valor na chave "test"
:
"scripts": {
"test": "mocha"
},
Arquivos da lição
Os arquivos que você viu durante o decorrer desta lição, podem ser encontrados neste link.
Conclusão
Nesta lição, você aprendeu a criar testes unitários utilizando o Mocha
e Chai
.
Não deixe de consultar a documentação de ambas as ferramentas para entender mais detalhes sobre o funcionamento de cada uma delas:
Até a próxima 😌