Neste post, vamos escrever o teste unitário do CRUD de usuários feito até aqui.
Como a nossa camada de serviço acessa a base de dados com o typeorm
, vamos escrever algumas funções que vai mockar
a instância do typeorm, facilitando reescrever o retorno do acesso ao banco.
Passo a passo
- Instalar as dependências (babel-jest, jest, jest-mock-extended, supertest, ts-jest) e seus types
- Configurar o Jest
- Escrever mocks de alguns middlewares, por exemplo de logs
- Escrever o mock do typeorm
- Implementar os testes
Instalações
yarn add -D babel-jest jest jest-mock-extended supertest ts-jest @types/jest @types/supertest
Configurações
O próprio jest tem uma função para montar o arquivo de configurações, como eu já uso a lib em vários projetos, vou copiar um padrão que eu costumo utilizar. Por estarmos usando o babel e import nomeado (@middleware, etc...) a config já está certinha ;D
jest.config.js
const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');
module.exports = {
clearMocks: true,
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>',
}),
coverageDirectory: 'coverage',
coverageReporters: ['lcov', 'html', 'text'],
coveragePathIgnorePatterns: [
'/node_modules/',
'src/tools',
'src/services',
'src/middlewares',
],
preset: 'ts-jest',
testEnvironment: 'node',
modulePathIgnorePatterns: ['dist', 'node_modules', 'coverage'],
testMatch: ['**/?(*.)+(spec|test).(js|ts|tsx)'],
};
Mocks
Mocking user modules
Simulações manuais são definidas por escrever um módulo em um subdiretório mocks/ imediatamente adjacente ao módulo. Por exemplo, para simular (mock, em inglês) um módulo chamado user no diretório models, crie um arquivo chamado user.js e coloque ele no diretório models/mocks.
Levando em conta a explicação da documentação do Jest, vamos mockar o middleware de logs.
src/middlewares/__mocks__/logger.ts
const logger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
silly: () => {},
};
export default logger;
Agora, quando o nosso teste passar por um log dentro da classe de serviço, não vai ser executado nada, deixando o console do teste mais limpo.
Mock Typeorm
Quando queremos mockar um módulo que foi instalado como dependênciacia, nós criamos a pasta __mocks__
na raiz do projeto, e dentro dela os arquivos com o nome da lib.
__mocks__/typeorm.ts
import { mock } from 'jest-mock-extended';
import { Repository, MongoRepository } from 'typeorm';
export const repositoryMock = mock<Repository<any>>();
export const mongoRepositoryMock = mock<MongoRepository<any>>();
export const getConnection = jest.fn().mockReturnValue({
getRepository: () => repositoryMock,
getMongoRepository: () => mongoRepositoryMock,
});
export class BaseEntity {}
export const ObjectIdColumn = () => {};
export const Column = () => {};
export const Index = () => {};
export const CreateDateColumn = () => {};
export const UpdateDateColumn = () => {};
export const Entity = () => {};
Aqui estamos mockando todos os recursos do typeorm que a aplicação está utilizando, decorators
, repositories
, funções
, etc...
Então lá na classe de serviço, onde importamos um repository no construtor, quando o teste for executado, é o objeto do arquivo acima que será utilizado. Dessa forma, no teste unitário eu consigo simular o retorno dos métodos de acesso ao banco, findOne
, find
, update
, delete
, etc...
Escrevendo o primeiro teste
Para os testes do crud, vou utilizar o supertest, ele simula a camada do express, e assim conseguimos fazer um request pra nossa api.
Vamos escrever nossos testes dentro uma pasta tests
na raiz do projeto, e então o separamos por módulos.
GET
Os testes unitários são executados em blocos de código, assim conseguimos separar cada bloco em um assunto específico, revise a documentação caso necessário
E para facilitar a escrita do testes, fazendo passar por todas as regras de negócio, eu costumo deixar a classe serviço aberta lado a lado do teste.
A primeira regra é: Se o usuário não existir na base de dados, a api devolve um erro com status 404.
Então vamos escrever esse teste
tests/User/user.test.ts
import { MockProxy } from 'jest-mock-extended';
import request from 'supertest';
import { MongoRepository } from 'typeorm';
jest.mock('typeorm');
jest.mock('../../src/middlewares/logger');
describe('## User Module ##', () => {
// Importamos a instância do express para usar com supertest
const { app } = require('../../src/app').default;
// Aqui é a instância do typeorm que vai na base de dados
const repository = require('typeorm').mongoRepositoryMock as MockProxy<
MongoRepository<any>
>;
// Vamos separar os endpoints do crud por blocos
describe('## GET ##', () => {
// Aqui vamos escrever os testes para o método findOne da classe de serviço
test('should return error when user does not exists', async () => {
// A condição para retornar esse erro é o retorno da base ser nulo
// Então vamos mocar o retorno do typeorm
// Assim quando o typeorm resolver a chamada findOne,
// o retorno é o objetos que passarmos no mock
repository.findOne.mockResolvedValue(null);
// Aqui estou fazendo um request para a api
await request(app)
.get('/api/users/some-id')
.expect(404, {
errors: [
{
code: 'USER_NOT_FOUND',
message: 'Usuário não encontrado',
status: 404,
},
],
});
});
});
});
No Vscode, instale a extenções Jest e Jest Runner
Com elas, podemos executar um teste específico clicando no botão Run
Agora, vamos escrever todos os outros testes do bloco ## GET ##
...
describe('## GET ##', () => {
test('should return error when user does not exists', async () => {
repository.findOne.mockResolvedValue(null);
await request(app)
.get('/api/users/some-id')
.expect(404, {
errors: [
{
code: 'USER_NOT_FOUND',
message: 'Usuário não encontrado',
status: 404,
},
],
});
});
test('should return an user', async () => {
const user = {
_id: '6001abf43d4675bc1aa693bc',
name: 'Teste',
password: '1234',
};
repository.findOne.mockResolvedValue(user);
await request(app).get('/api/users/some-id').expect(200, user);
});
});
...
Nosso CRUD, não tem tantas regras de negócio, mas é importante passar por todas elas, para simular o comportamento da api.
POST
Agora vamos escrever os testes da criação do usuário
async create(user: Users): Promise<Users> {
try {
const response = await this.repository.save(user);
return response;
} catch (e) {
if (e.code === 11000)
throw new CustomError({
code: 'USER_ALREADY_EXISTS',
message: 'Usuário já existente',
status: 409,
});
throw e;
}
}
Na classe de serviço, só temos uma regra, a de usuário ja existente. Mas temos um middleware para validar o payload recebido, os testes desse bloco deve cobrir todas essas regras.
O primeiro teste vai cair na validação de documento
...
describe('## POST ##', () => {
test('should return error when document is invalid', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Teste', document: '1234', password: '0123456789' })
.expect(400, {
errors: [
{
code: 'ValidationError',
message: 'document: deve conter exatamente 11 caracteres',
},
],
});
});
});
...
O segundo teste na validação do password
...
test('should return error when password is invalid', async () => {
await request(app)
.post('/api/users')
.send({
name: 'Teste',
document: '12345678900',
password: '1234',
})
.expect(400, {
errors: [
{
code: 'ValidationError',
message: 'password: valor muito curto (mínimo 6 caracteres)',
},
],
});
});
...
E um teste para validar a obrigatoriedade dos campos
...
test('should return error when payload is invalid', async () => {
await request(app)
.post('/api/users')
.send({})
.expect(400, {
errors: [
{ code: 'ValidationError', message: 'name é um campo obrigatório' },
{
code: 'ValidationError',
message: 'document é um campo obrigatório',
},
{
code: 'ValidationError',
message: 'password é um campo obrigatório',
},
],
});
});
...
Agora, o teste vai passar nas validações, mas cair na regra de usuário já existente
...
test('should return error when user already exists', async () => {
// Aqui vamos simular o erro de criação do usuário
repository.save.mockRejectedValue({
code: 11000,
});
await request(app)
.post('/api/users')
.send({
name: 'Teste',
document: '12345678900',
password: '1234567890',
})
.expect(409, {
errors: [
{
code: 'USER_ALREADY_EXISTS',
message: 'Usuário já existente',
status: 409,
},
],
});
});
...
Caindo na exception não tratada
...
test('should return error when create user', async () => {
repository.save.mockRejectedValue(new Error('Some Exception'));
await request(app)
.post('/api/users')
.send({
name: 'Teste',
document: '12345678900',
password: '1234567890',
})
.expect(500, {
errors: [{ code: 'E0001', message: 'Some Exception' }],
});
});
...
Agora o de sucesso
...
test('should create an user', async () => {
const user = {
name: 'Teste',
document: '12345678900',
password: '1234567890',
};
repository.save.mockResolvedValue({
...user,
_id: 'some-id',
});
await request(app).post('/api/users').send(user).expect(200, {
name: 'Teste',
document: '12345678900',
password: '1234567890',
_id: 'some-id',
});
});
...
Coverage
Antes de escrever os testes de UPDATE
e DELETE
. Vamos ver como está ficando a cobertura dos testes
No arquivo package.json
, vamos escrever um script que executa os testes e colhe a cobertura
package.json
{
...
"scripts": {
...
"coverage": "rimraf coverage && NODE_ENV=test jest --coverage --silent --detectOpenHandles --forceExit",
...
},
...
}
No terminal vamos executar
yarn coverage
Esse comando gerou uma pastinha chamada coverage
na raiz do projeto.
abra o arquivo index.html
dela no browser e vemos o resultado dos testes com a cobertura
Navegando até UserService
, podemos ver que já estamos com 77% de cobertura nesse arquivo, e os métodos create e findOne está totalmente coberto.
Não esqueca de adicionar a pasta
coverage
no arquivo.gitignore
, para não subi-la para o repositório
UPDATE e DELETE
...
describe('## PUT ##', () => {
test('should return error when user does not exists', async () => {
repository.updateOne.mockResolvedValue({} as any);
repository.findOne.mockResolvedValue(null);
await request(app)
.put('/api/users/6001abf43d4675bc1aa693bd')
.send({ name: 'Teste' })
.expect(404, {
errors: [
{
code: 'USER_NOT_FOUND',
message: 'Usuário não encontrado',
status: 404,
},
],
});
});
test('should return updated user', async () => {
const user = {
name: 'Teste',
document: '12345678900',
password: '1234567890',
};
repository.updateOne.mockResolvedValue({} as any);
repository.findOne.mockResolvedValue({
...user,
_id: '6001abf43d4675bc1aa693bd',
});
await request(app)
.put('/api/users/6001abf43d4675bc1aa693bd')
.send({ name: 'Teste' })
.expect(200, {
...user,
_id: '6001abf43d4675bc1aa693bd',
});
});
});
describe('## DELETE ##', () => {
test('should return error when user does not exists', async () => {
repository.findOne.mockResolvedValue(null);
await request(app)
.delete('/api/users/6001abf43d4675bc1aa693bd')
.send({ name: 'Teste' })
.expect(404, {
errors: [
{
code: 'USER_NOT_FOUND',
message: 'Usuário não encontrado',
status: 404,
},
],
});
});
test('should return deleted user', async () => {
const user = {
name: 'Teste',
document: '12345678900',
password: '1234567890',
};
repository.findOne.mockResolvedValue({
...user,
_id: '6001abf43d4675bc1aa693bd',
});
repository.deleteOne.mockResolvedValue({} as any);
await request(app)
.delete('/api/users/6001abf43d4675bc1aa693bd')
.send({ name: 'Teste' })
.expect(200, {
...user,
_id: '6001abf43d4675bc1aa693bd',
});
});
});
...
Agora com todos os testes executados, o coverage está em 100%
Considerações Finais
Pra finalizar, vamos escrever um script que executa todos os testes.
E ao realizar um commit, todos os testes serão executados e caso algum falhe, o commit será barrado.
Essa é uma boa prática, nos impede de subir alguma coisa que está falhando devido alguma alteração no código
package.json
{
...
"scripts": {
...
"test": "rimraf coverage && NODE_ENV=test jest --coverage --silent --detectOpenHandles --forceExit",
...
},
"husky": {
"hooks": {
"pre-commit": "npm test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
...
}
Agora, em todo commit teremos os testes sendo executados
O que está por vir
No próximo post, implementaremos uma camada de autenticação com JWT
Top comments (1)
Vitor, há um erro com Jest:
FAIL src/tests/user.test.ts
● Test suite failed to run