Criando a API
A ideia é criar uma API CRUD
de usuários bem simples, já que o foco aqui está nos testes. Você pode seguir o paso a passo ou simplesmente clonar o repositório do projeto.
Tecnologias utilizadas:
Typescript, POO, Express, Joi, MySQL, Prisma, NodeJs
Setup inicial
- Inicie o repositório do projeto
npm init -y
- Instale todas as dependências que vão ser necessárias nesse projeto
npm install @prisma/client express joi
npm install -D typescript ts-node ts-node-dev prisma @types/node @types/express
- Adicione os seguintes scripts ao
package.json
"scripts": {
"start": "ts-node src/server.ts",
"dev": "tsnd --exit-child /src/server.ts"
}
- Crie um
tsconfig.json
usando otsc
npx tsc --init
Exemplo do package.json
{
"name": "integration-test-api",
"version": "1.0.0",
"description": "",
"main": "server.ts",
"scripts": {
"start": "ts-node src/server.ts",
"dev": "tsnd --exit-child /src/server.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^17.0.23",
"prisma": "^3.12.0",
"ts-node": "^10.7.0",
"ts-node-dev": "^1.1.8",
"typescript": "^4.6.3"
},
"dependencies": {
"@prisma/client": "^3.12.0",
"express": "^4.17.3",
"joi": "^17.6.0"
}
}
Iniciando o Prisma
Prisma é um ORM que lida muito bem com Typescript, tem suporte para o MySQL e um documentação muito bem elaborada. Levando isso em conta, é ele que vamos utilizar neste artigo.
Primeiro de tudo é necessário criar o schema
do prisma. Para isso siga os passos abaixo:
- Crie a pasta
src/
, dentro dela crie outra pastaprisma/
e dentro desta crie o arquivoschema.prisma
src
└── prisma
└── schema.prisma
- No
schema.prisma
é onde vão estar as configurações necessárias do prisma para se comunicar ao MySQL. No nosso caso adicione o seguinte código ao arquivo
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model user {
id Int @id @default(autoincrement())
firstName String
lastName String
email String @unique
occupation String?
}
- Como o
schema.prisma
está num caminho diferente do padrão é necessário avisar isso ao prisma. Adicione a seguinte chave ao seupackage.json
"prisma": {
"schema": "src/prisma/schema.prisma"
}
- Agora é necessário inicar um container docker de MySQL (recomendado)
docker container run -d -e MYSQL_ROOT_PASSWORD=password -p 3336:3306 mysql:latest
- Próximo passo é criar um
.env
na raiz do projeto com a variávelDATABASE_URL
DATABASE_URL=mysql://root:password@localhost:3336/users_api
- Por último, crie uma migration do prisma
npx prisma migrate dev --name init
Construindo a API
Chegou a hora de codar a API em si. De início, como estamos usando o Typescript, crie as interfaces que serão usadas no projeto:
-
src/interfaces/index.ts
export interface IUser extends IUserCreateRequest {
id: number;
}
export interface IUserCreateRequest {
firstName: string;
lastName: string;
email: string;
occupation: string | undefined | null;
}
export interface IUserUpdateRequest extends Partial<IUserCreateRequest> {}
O próximo passo é criar algumas classes de erros personalizados que serão úteis quando algum erro esperado acontecer:
-
src/utils/errors/HttpError.ts
export default abstract class HttpError extends Error {
public abstract httpCode: number;
public abstract name: string;
}
-
src/utils/errors/BadRequest.ts
import HttpError from './HttpError';
export class BadRequest extends HttpError {
public httpCode: number;
public name: string;
constructor(message: string, httpCode = 400) {
super(message);
this.httpCode = httpCode;
this.name = 'BadRequest';
}
}
-
src/utils/errors/Conflict.ts
import HttpError from './HttpError';
export class Conflict extends HttpError {
public httpCode: number;
public name: string;
constructor(message: string, httpCode = 409) {
super(message);
this.httpCode = httpCode;
this.name = 'Conflict';
}
}
-
src/utils/errors/NotFound.ts
import HttpError from './HttpError';
export class NotFound extends HttpError {
public httpCode: number;
public name: string;
constructor(message: string, httpCode = 404) {
super(message);
this.httpCode = httpCode;
this.name = 'NotFound';
}
}
-
src/utils/errors/index.ts
export * from './BadRequest';
export * from './Conflict';
export * from './NotFound';
Para fins didáticos, neste projeto será usado uma classe que registra logs da API
(os logs são salvos no arquivo logs.txt
na raiz do projeto). Copie o código dela também:
-
src/utils/logger.ts
import fs from 'fs/promises';
import path from 'path';
export default class Logger {
private static _filePath = path.resolve(__dirname, '../../logs.txt');
public static async save(info: string): Promise<void> {
const sentence = `${new Date().toLocaleString('pt-BR')}: ${info}\n`;
fs.writeFile(Logger._filePath, sentence, { flag: 'a' });
}
}
Para fazer a validação dos dados das requisições, neste projeto, será ultilizado o Joi. Copie os middlewares que serão usados:
-
src/middlewares/index.ts
import { ErrorRequestHandler, RequestHandler } from 'express';
import joi from 'joi';
import { IUserCreateRequest, IUserUpdateRequest } from '../interfaces';
import { BadRequest } from '../utils/errors/BadRequest';
import HttpError from '../utils/errors/HttpError';
export default class Middlewares {
private static _createSchema = joi.object({
firstName: joi.string().required(),
lastName: joi.string().required(),
email: joi.string().email().required(),
occupation: joi.string().required(),
});
private static _updateSchema = joi.object({
firstName: joi.string(),
lastName: joi.string(),
email: joi.string().email(),
occupation: joi.string(),
});
public static error: ErrorRequestHandler = (err, _req, res, _next) => {
if (err instanceof HttpError) {
const { httpCode, message } = err;
return res.status(httpCode).json({ error: { message } });
}
return res.status(500).json({ error: { message: err.message } });
};
public static createValidation: RequestHandler = (req, _res, next) => {
const { email, firstName, lastName, occupation } = req.body as IUserCreateRequest;
const { error } = Middlewares._createSchema.validate({
email,
firstName,
lastName,
occupation,
});
if (error) return next(new BadRequest(error.message));
next();
};
public static updateValidation: RequestHandler = (req, _res, next) => {
const { email, firstName, lastName, occupation } = req.body as IUserUpdateRequest;
const { error } = Middlewares._updateSchema.validate({
email,
firstName,
lastName,
occupation,
});
if (error) return next(new BadRequest(error.message));
next();
};
}
Partindo para a construção do user
, ele vai ser dividido em 4 partes:
-
repository
: interface de comunicação com o prisma; -
service
: lógica de negócio; -
controller
: middleware resposta do express, onde vai ser chamado oLogger
; -
router
: estrutura dos endpoints.
Copie os códigos abaixo:
-
src/user/repository.ts
import { PrismaClient } from '@prisma/client';
import { IUser, IUserCreateRequest, IUserUpdateRequest } from '../interfaces';
export default class UserRepository {
private _prisma: PrismaClient;
constructor(prisma = new PrismaClient()) {
this._prisma = prisma;
}
public async getAll(): Promise<IUser[]> {
return this._prisma.user.findMany();
}
public async getById(id: number): Promise<IUser | null> {
return this._prisma.user.findUnique({ where: { id } });
}
public async getByEmail(email: string): Promise<IUser | null> {
return this._prisma.user.findUnique({ where: { email } });
}
public async create(newUser: IUserCreateRequest): Promise<IUser> {
return this._prisma.user.create({ data: newUser });
}
public async update(id: number, payload: IUserUpdateRequest): Promise<IUser> {
return this._prisma.user.update({ where: { id }, data: payload });
}
public async delete(id: number): Promise<IUser> {
return this._prisma.user.delete({ where: { id } });
}
}
-
src/user/service.ts
import { IUser, IUserCreateRequest, IUserUpdateRequest } from '../interfaces';
import { Conflict, NotFound } from '../utils/errors';
import UserRepository from './repository';
export default class UserService {
private _repository: UserRepository;
constructor(repository = new UserRepository()) {
this._repository = repository;
}
public async getAll(): Promise<IUser[]> {
return this._repository.getAll();
}
public async getById(id: number): Promise<IUser> {
const user = await this._repository.getById(id);
if (!user) throw new NotFound('user not found');
return user;
}
public async create(newUser: IUserCreateRequest): Promise<IUser> {
const userExists = await this._repository.getByEmail(newUser.email);
if (userExists) throw new Conflict('user already exists');
return this._repository.create(newUser);
}
public async update(id: number, payload: IUserUpdateRequest): Promise<IUser> {
const user = await this._repository.getById(id);
if (!user) throw new NotFound('user not found');
return this._repository.update(id, payload);
}
public async delete(id: number): Promise<IUser> {
const user = await this._repository.getById(id);
if (!user) throw new NotFound('user not found');
return this._repository.delete(id);
}
}
-
src/user/controller.ts
import { RequestHandler } from 'express';
import { IUser, IUserCreateRequest, IUserUpdateRequest } from '../interfaces';
import { BadRequest } from '../utils/errors/BadRequest';
import Logger from '../utils/logger';
import UserService from './service';
export default class UserController {
private _service: UserService;
constructor(service = new UserService()) {
this._service = service;
}
public getAll: RequestHandler = async (_req, res, next) => {
try {
const allUsers = await this._service.getAll();
res.status(200).json(allUsers);
Logger.save('getAll() success');
} catch (error) {
Logger.save('getAll() fail');
next(error);
}
};
public getById: RequestHandler = async (req, res, next) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) return next(new BadRequest('invalid id'));
try {
const user = await this._service.getById(id);
res.status(200).json(user);
Logger.save('getById() success');
} catch (error) {
Logger.save('getById() fail');
next(error);
}
};
public create: RequestHandler = async (req, res, next) => {
const { email, firstName, lastName, occupation } = req.body as IUserCreateRequest;
try {
const newUser = await this._service.create({ email, firstName, lastName, occupation });
res.status(201).json(newUser);
Logger.save('create() success');
} catch (error) {
Logger.save('create() fail');
next(error);
}
};
public update: RequestHandler = async (req, res, next) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) return next(new BadRequest('invalid id'));
const { email, firstName, lastName, occupation } = req.body as IUserUpdateRequest;
try {
const updatedUser = await this._service.update(id, {
email,
firstName,
lastName,
occupation,
});
res.status(200).json(updatedUser);
Logger.save('update() success');
} catch (error) {
Logger.save('update() fail');
next(error);
}
};
public delete: RequestHandler = async (req, res, next) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) return next(new BadRequest('invalid id'));
try {
await this._service.delete(id);
res.status(204).end();
Logger.save('delete() success');
} catch (error) {
Logger.save('delete() fail');
next(error);
}
};
}
-
src/user/router.ts
import { Router } from 'express';
import Middlewares from '../middlewares';
import UserController from './controller';
export default class UserRouter {
private _router: Router;
private _controller: UserController;
constructor(router = Router(), controller = new UserController()) {
this._router = router;
this._controller = controller;
this._router.get('/', this._controller.getAll);
this._router.get('/:id', this._controller.getById);
this._router.post('/', Middlewares.createValidation, this._controller.create);
this._router.put('/:id', Middlewares.updateValidation, this._controller.update);
this._router.delete('/:id', this._controller.delete);
}
get router() {
return this._router;
}
}
Por fim, é preciso criar o express app
e exportá-lo para ser acessível aos testes.
Obs.: Como instanciar um UserRouter
é muito verboso isso vai ser feito por uma classe especial, a Factory
.
-
src/factory.ts
import { PrismaClient } from '@prisma/client';
import { Router } from 'express';
import UserController from './user/controller';
import UserRepository from './user/repository';
import UserRouter from './user/router';
import UserService from './user/service';
export default class Factory {
private static _prisma = new PrismaClient();
public static get userRouter() {
const userRepository = new UserRepository(Factory._prisma);
const userService = new UserService(userRepository);
const userController = new UserController(userService);
const userRouter = new UserRouter(Router(), userController);
return userRouter.router;
}
}
-
src/app.ts
import express from 'express';
import Factory from './factory';
import Middlewares from './middlewares';
const app = express();
app.use(express.json());
app.use('/user', Factory.userRouter);
app.use(Middlewares.error);
export default app;
-
src/server.ts
import app from './app';
const PORT = 3000;
app.listen(PORT, () => {
console.log('Server online');
console.log(`PORT ${PORT}`);
});
Pronto, a API está feita. Agora vem a parte boa, os testes. 🧪
Testes
Teste de integração é quando testa-se vários pedaços do código de uma vez. O objetivo é testar se a interação entre eles está produzindo o resultado esperado.
Existe uma discussão sobre se faz sentido ou não mockar as operações de IO
em testes de integração. Não tem nada escrito em pedra. Sendo assim, neste artigo, será mostrado como criar testes de integração mockando as operações de IO
. Eu planejo criar, nas próximas semanas, um artigo onde não terá mocks mas sim um banco de dados temporário apenas para os testes.
Para este projeto o UserRepository
será mockado para não haver consultas ao banco de dados (prisma) e o Logger
para não haver manipulação de arquivos (fs).
Tecnologias utilizadas:
Typescript, mocha, chai, sinon
Setup inicial
- Baixe os pacotes que serão utilizados
npm i -D mocha @types/mocha chai chai-http @types/chai sinon @types/sinon
- Adicione o seguinte script ao
package.json
"scripts": {
...
"test": "mocha --require ts-node/register __tests__/**/*.test.ts --exit"
}
- Crie a pasta
__tests__
na raiz do projeto
Contexto
Antes, vale a pena pensar:
"O que é que eu vou testar?"
O que vai ser testado é a resposta da API dependo da requisição que é feita.
Exemplo: O endpoint GET /user
retorna um array de usuários, sendo assim da para criar os seguintes testes:
A requisição deu certo?
- se sim
- o status http da resposta é
200
como eu esperava? - o body da resposta é um array?
- o body da resposta é um array de usuários?
- o
Logger
foi chamado com"getAll() success"
?
- o status http da resposta é
- se não
- o status http da resposta é
500
como eu esperava? - o body da resposta contém o objeto
error
? - objeto
error
contém a mensagem que eu esperava? - o
Logger
foi chamado com"getAll() fail"
?
- o status http da resposta é
"Como é que eu vou testar isso?"
Usando as ferramentas que o mocha
, chai
e sinon
nos oferece:
- mocha
- é o
test runner
, ou seja, a ferramenta que executa os testes em si com comandos de terminal; - oferece o
describe
que cria um contexto de testes, agrupa testes e agrupa até mesmo outrosdescribe
; - oferece o
it
(outest
, não tem diferença) que é onde se escreve o teste em si.
- é o
- chai
- oferece o
expect
, uma ferramenta de asserção que é peça fundamental para os testes; - oferece matchers muito semânticos;
- tem vários plugins que adicionam ferramentas muito úteis e um deles vai ser usado aqui, o
chai-http
.
- oferece o
- sinon
- oferece o
sinon.stub
, ferramenta de mockar, simular o comportamento de uma função ou método.
- oferece o
Testando o GET /user
Traduzindo as perguntas acima para código:
-
__tests__/user.test.ts
import chai from 'chai';
import sinon from 'sinon';
import chaiHttp from 'chai-http';
import UserRepository from '../src/user/repository';
import app from '../src/app';
import * as fakeData from './fakeData';
import Logger from '../src/utils/logger';
chai.use(chaiHttp);
const { expect } = chai;
describe('em caso de sucesso', () => {
before(() => {
sinon.stub(UserRepository.prototype, 'getAll').resolves(fakeData.get.mock);
sinon.stub(Logger, 'save').resolves();
});
after(() => {
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar um array de usuários e enviar status 200', async () => {
const { status, body } = await chai.request(app).get('/user');
expect(status).to.be.equal(200);
expect(body).to.be.an('array');
expect(body).to.be.deep.equal(fakeData.get.response);
expect((Logger.save as sinon.SinonStub).calledWith('getAll() success')).to.be.true;
});
});
Entendendo o teste
- Import do
app
do express (app = express()
) para fazer as requisições
...
import app from '../src/app';
...
- Aqui vai ver usado um arquivo para agrupar mocks, requests e responses
...
import * as fakeData from './fakeData';
...
copie o fakeData
fakeData
__tests__/fakeData/index.ts
import { IUser } from '../../src/interfaces';
const homer: IUser = {
id: 1,
firstName: 'Homer',
lastName: 'Simpson',
email: 'homer@gmail.com',
occupation: 'nuclear safety inspector',
};
const ragnar: IUser = {
id: 2,
firstName: 'Ragnar',
lastName: 'Lodbrok',
email: 'ragnar@gmail.com',
occupation: 'king',
};
const eren: IUser = {
id: 3,
firstName: 'Eren',
lastName: 'Yeager',
email: 'eren.yeager@gmail.com',
occupation: 'soldier',
};
const morty: IUser = {
id: 4,
firstName: 'Morty',
lastName: 'Smith',
email: 'msmith.125@gmail.com',
occupation: 'student',
};
export const get = {
mock: [homer, ragnar, eren, morty],
response: [homer, ragnar, eren, morty],
};
- Insere o plugin
chai-http
para ser usado pelo chai e pega oexpect
, que é basicamente o que é usado
...
chai.use(chaiHttp);
const { expect } = chai;
...
- Aqui o
describe
está sendo usado para criar um contexto de testes, ou seja, para agrupar obefore
,after
eit
de forma isolada
describe('em caso de sucesso', () => {
...
});
- O
before
executa uma callback antes do teste e é geralmente usado para fazer setup. Aqui o setup será mockar oUserRepository
e oLogger
.
before(() => {
sinon.stub(UserRepository.prototype, 'getAll').resolves(fakeData.get.mock);
sinon.stub(Logger, 'save').resolves();
});
O que mockar o UserRepository
significa? Quer dizer que vamos retirar a implementação original e fazer o método getAll()
resolver (porque o método retorna uma Promise
) naquilo que esperamos que ele resolva. O mesmo vale para o Logger
.
Podemos fazer o método mockado resolver qualquer coisa, mas é preciso pensar bem para que o mock reflita a realidade porque, se não, os testes seriam inúteis. No caso do UserRepository.getAll()
, qual valor ele poderia resolver que refletiria a realidade? Um array de usuários.
Como se mocka um método de uma classe? Usando o sinon.stub
:
- Para mockar métodos estáticos:
sinon.stub(classe, 'método')
// exemplo
sinon.stub(Logger, 'save').resolves();
- Para mockar métodos não estáticos:
sinon.stub(classe.prototype, 'método')
// exemplo
sinon.stub(UserRepository.prototype, 'getAll').resolves(fakeData.get.mock);
- O
after
executa uma callback depois do teste e é geralmente usado para fazer teardown. Aqui o teardown será reverter o que foi mockado para não ter efeitos colaterais sobre outros testes.
after(() => {
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
Como se reverte o que foi mockado? Usando o restore()
do sinon. Como estamos usando Typescript é necessário fazer a conversão do método mockado para sinon.SinonStub
porque o Typescript, por padrão, não o entende como um stub.
- Para métodos estáticos:
(classe.método as sinon.SinonStub).restore()
// exemplo
(Logger.save as sinon.SinonStub).restore();
- Para métodos não estáticos:
(classe.prototype.método as sinon.SinonStub).restore()
// exemplo
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
- O
it
é onde está o teste em si. Como questionado aqui, o que o teste está fazendo, é:
it('deve retornar um array de usuários e enviar status 200', async () => {
// faz a requisição para a rota GET /user
const { status, body } = await chai.request(app).get('/user');
// testa se o status é igual a 200
expect(status).to.be.equal(200);
// testa se o body da resposta é um array
expect(body).to.be.an('array');
// testa se o body da resposta é estritamente igual ao esperado
expect(body).to.be.deep.equal(fakeData.get.response);
// testa se Logger.save foi chamado como: Logger.save('getAll() success')
expect((Logger.save as sinon.SinonStub).calledWith('getAll() success')).to.be.true;
});
O teste do caso de sucesso está feito, mas e no caso onde acontece um erro?
describe('em caso de erro no banco de dados', () => {
before(() => {
// mocka o UserRepository para que lance um erro
sinon.stub(UserRepository.prototype, 'getAll').throws(new Error('db error'));
// mocka o Logger
sinon.stub(Logger, 'save').resolves();
});
after(() => {
// restaura o UserRepository
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
// restaura o Logger
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar a mensagem do erro e enviar status 500', async () => {
// faz a requisição para a rota GET /user
const { status, body } = await chai.request(app).get('/user');
// testa se o status é igual a 500
expect(status).to.be.equal(500);
// testa se o body da resposta tem propriedade error
expect(body).to.have.property('error');
//testa se no body da resposta error.message é o que se espera
expect(body.error.message).to.be.equal('db error');
// testa se Logger.save foi chamado como: Logger.save('getAll() fail')
expect((Logger.save as sinon.SinonStub).calledWith('getAll() fail')).to.be.true;
});
});
Por fim, vamos agrupar o teste do caso de sucesso e do caso de falha num describe:
describe('GET /user', () => {
describe('em caso de sucesso', () => {
before(() => {
sinon.stub(UserRepository.prototype, 'getAll').resolves(fakeData.get.mock);
sinon.stub(Logger, 'save').resolves();
});
after(() => {
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar um array de usuários e enviar status 200', async () => {
const { status, body } = await chai.request(app).get('/user');
expect(status).to.be.equal(200);
expect(body).to.be.an('array');
expect(body).to.be.deep.equal(fakeData.get.response);
expect((Logger.save as sinon.SinonStub).calledWith('getAll() success')).to.be.true;
});
});
describe('em caso de erro no banco de dados', () => {
before(() => {
sinon.stub(UserRepository.prototype, 'getAll').throws(new Error('db error'));
sinon.stub(Logger, 'save').resolves();
});
after(() => {
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar a mensagem do erro e enviar status 500', async () => {
const { status, body } = await chai.request(app).get('/user');
expect(status).to.be.equal(500);
expect(body).to.have.property('error');
expect(body.error.message).to.be.equal('db error');
expect((Logger.save as sinon.SinonStub).calledWith('getAll() fail')).to.be.true;
});
});
});
Rode os testes com o npm test
.
Testando PUT /user
Testar o método PUT
é um pouco mais complexo porque ele usa 2 métodos do repository
e envia um body na requisição.
-
__tests__/fakeData/index.ts
...
const ragnar: IUser = {
id: 2,
firstName: 'Ragnar',
lastName: 'Lodbrok',
email: 'ragnar@gmail.com',
occupation: 'king',
};
const updatedRagnar: IUser = {
id: 2,
firstName: 'Bjorn',
lastName: 'Ironside',
email: 'ragnar@gmail.com',
occupation: 'king',
};
...
export const put = {
getByIdMock: ragnar, // mock do UserRepository.getById()
mock: updatedRagnar, // mock do UserRepository.update()
request: { // body da requisição
firstName: updatedRagnar.firstName,
lastName: updatedRagnar.lastName,
},
response: updatedRagnar, // body da resposta
};
...
-
__tests__/user.test.ts
...
describe('PUT /user/:id', () => {
describe('caso o usuário exista', () => {
before(() => {
// mock do UserRepository.update
sinon.stub(UserRepository.prototype, 'update').resolves(fakeData.put.mock);
// mock do UserRepository.getById
sinon.stub(UserRepository.prototype, 'getById').resolves(fakeData.put.getByIdMock);
// mock do Logger
sinon.stub(Logger, 'save').resolves();
});
after(() => {
// reverte os mocks
(UserRepository.prototype.update as sinon.SinonStub).restore();
(UserRepository.prototype.getById as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar o usuário atualziado e enviar status 200', async () => {
// id = 2 -> ragnar
// faz a requisição, o método 'send' serve para enviar um body na requisição
const { status, body } = await chai.request(app).put('/user/2 ').send(fakeData.put.request);
// testa se o status http é 200
expect(status).to.be.equal(200);
// testa se o body da resposta é um objeto
expect(body).to.be.an('object');
// testa se o body da resposta é estritamente igual ao que se espera
expect(body).to.be.deep.equal(fakeData.put.response);
// testa se Logger.save foi chamado como: Logger.save('update() success')
expect((Logger.save as sinon.SinonStub).calledWith('update() success')).to.be.true;
});
});
describe('caso o usuário não exista', () => {
before(() => {
// mock do UserRepository.update
sinon.stub(UserRepository.prototype, 'update').resolves(fakeData.put.mock);
// mock do UserRepository.getById, null indica que o id não está cadastrado
sinon.stub(UserRepository.prototype, 'getById').resolves(null);
// mock do Logger
sinon.stub(Logger, 'save').resolves();
});
after(() => {
// reverte os mocks
(UserRepository.prototype.update as sinon.SinonStub).restore();
(UserRepository.prototype.getById as sinon.SinonStub).restore();
(Logger.save as sinon.SinonStub).restore();
});
it('deve retornar a mensagem do erro e enviar status 404', async () => {
// id = 9999
// faz a requisição, o método 'send' serve para enviar um body na requisição
const { status, body } = await chai.request(app).put('/user/9999').send(fakeData.put.mock);
// testa se o status http é 404
expect(status).to.be.equal(404);
// testa se o body da resposta tem a propriedade error
expect(body).to.have.property('error');
// testa se error.message é igual ao que se espera
expect(body.error.message).to.be.equal('user not found');
// testa se Logger.save foi chamado como: Logger.save('update() fail')
expect((Logger.save as sinon.SinonStub).calledWith('update() fail')).to.be.true;
});
});
});
Para ver todos os testes criados para o PUT /user
dê uma olhada no repositório.
Próximos passos
Tente criar testes para as outras rotas da API
. Algumas dicas que podem ajudar:
- antes de começar a escrever o teste pense no que você vai estar testando, como foi feito aqui;
- enquanto estiver escrevendo os testes, deixe o container MySQL do docker inativo para evitar falso-positivo;
- em
POST /user
o service chama 2 métodos doUserRepository
, então pense nisso quando for criar os mocks; - use e alimente o
fakeData
com mais objetos; - dê uma olhada nos recursos adicionais;
- você sempre pode consultar o repositório do projeto.
Referências
Considerações finais
Espero que tenham gostado do artigo. Qualquer dúvida é só perguntar aqui em baixo e eu tentarei ao máximo responder!
Github: @matheusg18
Linkedin: @matheusg18
Top comments (3)
Excelente post, bem explicado! Me ajudou a consultar alguns detalhes durante o teste da API que estava fazendo XD
Parabéns cara! Muito bem escrito, vai me ajudar muito!
Love it!