[CONTEUDO] Configuração de Testes E2E no NestJS com TypeORM e Postgres

Eu passei por alguns sufucos montando o setup para testes E2E no NestJS, mas finalmente consegui e quero compartilhar como fiz. Vai que sirva de inspiração para alguém.

Obs: É necessário saber um pouco de Jest e SuperTest além de Nest para entender as explicações abaixo.

Descrição do Ambiente de Testes

A lógica estará centralizado no setup.ts, que será executado antes de cada arquivo de teste, preparando o ambiente realizando as seguintes ações:

  • beforeAll: Executado antes de todos os testes

    1. Cria um banco de dados com nome aleatório para evitar conflitos com outros arquivos de testes.
    2. Altera as variáveis de ambiente para o modo de teste.
    3. Configura um módulo Nest para ficar igual ao configurado no main.ts.
    4. Inicializa esse módulo Nest e exporta um objeto que faz referência a esse módulo para ser usado nos testes.
  • beforeEach: Executado antes de cada teste, utilizando o TypeORM configurado no Nest

    1. Limpa o banco de dados, excluindo todas as tabelas/entidades (dropDatabase).
    2. Recria as tabelas/entidades (synchronize).
  • afterAll: Executado após todos os testes

    1. Finaliza o módulo Nest.
    2. Exclui o banco de dados criado.

Criando o Setup

Estrutura do Projeto

ROOT_DIR
├── src
├── test
│   ├── auth
│   ├── user
│   ├── createDb.ts
│   ├── jest-e2e.json
│   └── setup.ts
└── tsconfig.json

1 - Criação e Exclusão do Banco de Dados

Vamos criar um arquivo chamado createDb.ts, no qual criaremos uma classe que herda de DataSource do TypeORM. Ela será responsável por:

  • Fazer uma conexão com o servidor Postgres.
  • Criar um nome aleatorio para o banco de dados.
  • Ter os métodos para criar e deletar esse banco de dados.

createDb.ts:

export class DbTest extends DataSource {
  public dbName: string;
  constructor() {
    // configuramos o DataSource
    super({
      type: 'postgres',
      username: process.env['DB_USER'],
      password: process.env['DB_PASSWORD'],
      host: process.env['DB_HOST'],
      port: +process.env['DB_PORT'],
      // conectamos no database padrão
      database: 'postgres',
    });

    // criamos um nome aleatorio para o banco
    this.dbName = `TEST_DB_PAIA_${Date.now() + Math.floor(Math.random() * 100)}`;
  }
  
  // conectamos (initialize) no servidor antes de executar a query, quando acabamos 
  // fechamos (destroy) a conexão

  async create() {
    await this.initialize();
    await this.query(`CREATE DATABASE "${this.dbName}"`);
    await this.destroy();
  }

  async delete() {
    await this.initialize();
    await this.query(`DROP DATABASE "${this.dbName}"`);
    await this.destroy();
  }
}

Foi chato descobrir que o TypeORM não se conecta no servidor do PostgreSQL sem um banco de dados existente. E para conseguir criar um banco de dados pelo TypeORM, é necessário se conectar a um banco de dados diferente, e através dele criar o banco de dados desejado. Criei a classe acima para lidar com isso, utilizando o TypeORM configurado no Nest apenas para criar e limpar as entidades/tabelas.

2 - Criando o Setup

Criaremos o setup.ts, que rodará antes de cada arquivo de testes.

// exportamos um objeto para fazer referencia o moduleNest (app)
export const testRef = {} as {
  app: INestApplication;
};

// criamos uma instancia da classe feita no Passo 1
const dbTest = new DbTest();

// Testes
beforeAll(async () => {
  // criamos o database
  await dbTest.create();
  // setamos as variaveis
  process.env['MODO'] = 'test';
  process.env['DB_NAME'] = dbTest.dbName;

  // criamos o Module de teste do Nest importando o AppModule do projeto
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();

  // criamos um appNest e definimos dentro do objeto
  testRef.app = moduleFixture.createNestApplication();
  
  // configuramos as config adicionais que temos no main.ts nessa parte do código
  testRef.app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

  // Inicia o aplicativo Nest
  await testRef.app.init();
});


// Pegamos o serviço DataSource do TypeOrmModule e com ele limpamos e sincronizamos o 
// banco de dados antes de cada teste.
// Resumidamente estamos excluindo todas as tabelas(dropDatabase) e recriando (synchronize)
beforeEach(async () => {
  await testRef.app.get(DataSource).dropDatabase();
  await testRef.app.get(DataSource).synchronize();
});


// Fechamos o servidor e deletamos o database após todos os testes
afterAll(async () => {
  await testRef.app.close();
  await dbTest.delete();
});

3 - Configuração do Jest

Agora temos que configurar o Jest em jest-e2e.json

{
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "..",
  // defina os aliases de import definido no projeto aqui. 
  "moduleNameMapper": {
    // coloque esses dois
    "^src/(.*)$": "<rootDir>/src/$1",
    "test/setup": "<rootDir>/test/setup.ts"
  },
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": [
      "ts-jest",
      {
       // Desabilita a verificação de tipo do TS, deixando os testes mais rápidos (opcional)
        "isolatedModules": true
      }
    ]
  },
  // Defina os arquivos rodados antes dos arquivos de testes
  "setupFilesAfterEnv": [
    "./test/setup.ts"
  ]

4 - Criando o primeiro teste

user.e2e-spec.ts:

import { UserService } from 'src/user/user.service';
import * as request from 'supertest';
import { testRef } from 'test/setup';

const user = {
  email: 'leoPaia@gmail.com',
  password: 'leo123123',
  username: 'paia123',
};

describe('/user (POST)', () => {
  // O 'testRef.app.getHttpServer()' retorna um servidor HTTP do projeto. 
  // Utilizamos o Supertest para testar esse servidor.
  // Recomendo fazer uma função pois usa em todo teste
  const reqCreateUser = (userDto = user) =>
    request(testRef.app.getHttpServer()).post('/user').send(userDto);

  //   TESTES

  it('usuario criado corretamente', async () => {
    const res = await reqCreateUser();

    expect(res.statusCode).toBe(201);
  });

  it('o nome já tinha sido registrado', async () => {
    // Pelo método get do app(nestApp) eu consigo acessar os serviços dos meus módulos.
    // Estou acessando o UserService para criar um usuário diretamente pelo serviço.
    await testRef.app
      .get(UserService)
      .create({ ...user, email: 'emailDiferente@gmail.com' });
    const res = await reqCreateUser();

    expect(res.statusCode).toBe(400);
    expect(res.body).toHaveProperty('message', 'Esse nome já foi usado');
  });

  it('o email já tinha sido registrado', async () => {
    await testRef.app
      .get(UserService)
      .create({ ...user, username: 'nome diferente' });
    const res = await reqCreateUser();

    expect(res.statusCode).toBe(400);
    expect(res.body).toHaveProperty('message', 'Esse email já foi usado');
  });
});

Usei o arquivo mais simples. Se quiser mais exemplos, recomendo ver o repositório abaixo.

Fontes