Testes de integração para API com Typescript, mocha, chai e sinon
Tags: typescript, testing, backend
Published: 2022-04-14 17:18:28

[Pular para os testes](#testes)

## 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](https://github.com/matheusg18/integration-test-api).

Tecnologias utilizadas:

> Typescript, POO, Express, Joi, MySQL, Prisma, NodeJs

### Setup inicial

- Inicie o repositório do projeto

```bash
npm init -y
```

- Instale todas as dependências que vão ser necessárias nesse projeto

```bash
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`

```json
"scripts": {
  "start": "ts-node src/server.ts",
  "dev": "tsnd --exit-child /src/server.ts"
}
```

- Crie um `tsconfig.json` usando o `tsc`

```bash
npx tsc --init
```

{% details Exemplo do package.json %}

```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"
  }
}
```

{% enddetails %}

### Iniciando o Prisma

[Prisma](https://www.prisma.io/docs/) é 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 pasta `prisma/` e dentro desta crie o arquivo `schema.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

```prisma
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 seu `package.json`

```json
"prisma": {
  "schema": "src/prisma/schema.prisma"
}
```

- Agora é necessário inicar um container docker de MySQL (**recomendado**)

```bash
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ável [`DATABASE_URL`](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/connect-your-database-typescript-mysql)

```env
DATABASE_URL=mysql://root:password@localhost:3336/users_api
```

- Por último, crie uma migration do prisma

```bash
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`

```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`

```ts
export default abstract class HttpError extends Error {
  public abstract httpCode: number;

  public abstract name: string;
}
```

- `src/utils/errors/BadRequest.ts`

```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`

```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`

```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`

```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`

```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](https://joi.dev/api/). Copie os middlewares que serão usados:

- `src/middlewares/index.ts`

```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 o `Logger`;
- `router`: estrutura dos endpoints.

Copie os códigos abaixo:

- `src/user/repository.ts`

```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`

```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`

```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`

```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`

```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`

```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`

```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

```bash
npm i -D mocha @types/mocha chai chai-http @types/chai sinon @types/sinon
```

- Adicione o seguinte script ao `package.json`

```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"`?
- 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"`?

> **"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 outros `describe`;
  - oferece o `it` (ou `test`, não tem diferença) que é onde se escreve o teste em si.
- 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`.
- sinon
  - oferece o `sinon.stub`, ferramenta de _mockar_, simular o comportamento de uma função ou método.

### Testando o GET /user

Traduzindo as perguntas acima para código:

- `__tests__/user.test.ts`

```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

```ts
...
import app from '../src/app';
...
```

- Aqui vai ver usado um arquivo para agrupar _mocks_, requests e responses

```ts
...
import * as fakeData from './fakeData';
...
```

{% details copie o fakeData %}

#### fakeData

- `__tests__/fakeData/index.ts`

```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],
};
```

{% enddetails %}

- Insere o plugin `chai-http` para ser usado pelo chai e pega o `expect`, que é basicamente o que é usado

```ts
...
chai.use(chaiHttp);
const { expect } = chai;
...
```

- Aqui o `describe` está sendo usado para criar um contexto de testes, ou seja, para agrupar o `before`, `after` e `it` de forma isolada

```ts
describe('em caso de sucesso', () => {
  ...
});
```

- O `before` executa uma callback **antes** do teste e é geralmente usado para fazer _setup_. Aqui o _setup_ será _mockar_ o `UserRepository` e o `Logger`.

```ts
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`](https://sinonjs.org/releases/latest/stubs/):

- Para _mockar_ métodos estáticos: `sinon.stub(classe, 'método')`

```ts
// exemplo
sinon.stub(Logger, 'save').resolves();
```

- Para _mockar_ métodos **não** estáticos: `sinon.stub(classe.prototype, 'método')`

```ts
// 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**.

```ts
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()`

```ts
// exemplo
(Logger.save as sinon.SinonStub).restore();
```

- Para métodos **não** estáticos: `(classe.prototype.método as sinon.SinonStub).restore()`

```ts
// exemplo
(UserRepository.prototype.getAll as sinon.SinonStub).restore();
```

---

- O `it` é onde está o teste em si. Como questionado [aqui](#contexto), o que o teste está fazendo, é:

```ts
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;
});
```

[Mais sobre chai matchers](https://www.chaijs.com/api/bdd/)

[Dê uma olhada no fakeData](#fakedata)

O teste do caso de sucesso está feito, mas e no caso onde acontece um erro?

```ts
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:

```ts
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`

```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`

```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](https://github.com/matheusg18/integration-test-api/blob/main/__tests__/user.test.ts#L320).

### 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](#contexto);
- 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 do `UserRepository`, então pense nisso quando for criar os _mocks_;
- use e alimente o `fakeData` com mais objetos;
- dê uma olhada nos [recursos adicionais](#referências);
- você sempre pode consultar o [repositório do projeto](https://github.com/matheusg18/integration-test-api).

## Referências

- [mocha](https://mochajs.org/)
- [chai](https://www.chaijs.com/api/bdd/)
- [sinon](https://sinonjs.org/releases/latest/stubs/)
- [docker MySQL](https://hub.docker.com/_/mysql)

## 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](https://github.com/matheusg18)
Linkedin: [@matheusg18](https://www.linkedin.com/in/matheusg18/)