[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
- Cria um banco de dados com nome aleatório para evitar conflitos com outros arquivos de testes.
- Altera as variáveis de ambiente para o modo de teste.
- Configura um módulo Nest para ficar igual ao configurado no
main.ts
. - 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
- Limpa o banco de dados, excluindo todas as tabelas/entidades (
dropDatabase
). - Recria as tabelas/entidades (
synchronize
).
- Limpa o banco de dados, excluindo todas as tabelas/entidades (
-
afterAll: Executado após todos os testes
- Finaliza o módulo Nest.
- 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: '[email protected]',
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: '[email protected]' });
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
- Repositorio do projeto: Damas-Paia_backend