Test de integración con TypeORM

Iván Ramos
5 min readDec 26, 2022
Photo by Emile Perron on Unsplash

Llega el momento en el que los test unitarios no cubren todas nuestras necesidades y se necesita ir un poco mas allá para realizar test de integración involucrando a la base de datos.

Aclarar antes de comenzar que este documento es valido para versiones 0.3.6 de TypeORM o superior, ya que algunas configuraciones del DataSource pueden ser diferentes en versiones anteriores y precisamente este es uno de los motivos por el que creo este articulo.

En mi caso utilizo Express con TypeORM, veremos:

  • Como configurar nuestro entorno con TypeORM para realizar test e2e
  • Migraciones TypeORM
  • Finalmente crearemos un test e2e

Asumo que ya tenemos instalado TypeORM junto con TypeORM CLI en una versión superior a 0.3.6 con Jest, Supertest y Express.

Configuración:

Antes de nada nos aseguraremos que tenemos estas líneas en tsconfig.json:

"emitDecoratorMetadata": true,
"experimentalDecorators": true,

Esto activa los decoradores en Typescript que son necesarios para TypeORM.

Necesitamos establecer un origen de datos, para ello creamos el data-source.ts, dentro del directorio ‘src’.

// data-soruce.ts
import dotenv from 'dotenv';
import { DataSource } from 'typeorm';
import User from './entity/User';

dotenv.config();

const DBOrigin = new DataSource({
type: 'mysql',
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DB,
migrationsRun: true,
entities: [User],
migrations: ['./src/migrations/**/*.ts'],
});

const DBMemory = new DataSource({
type: 'sqlite',
database: ':memory:',
dropSchema: true,
entities: [User],
migrationsRun: true,
migrations: ['./src/migrations/**/*.ts'],
logging: false,
});

const AppDataSource = process.env.NODE_ENV !== 'test' ? DBOrigin : DBMemory;

export default AppDataSource;

Como vemos tenemos dos DataSource:

  • DBOrigin es el que nos provee la conexión en desarrollo o producción. Observa que se utiliza ‘mysql’ como driver y que tenemos parámetros como host, port, username, password y database que se obtienen de nuestro .env.
  • DBMemory sera nuestro DataSource para test end to end. Se levanta una base de datos en memoria que se iniciara cada vez que se ejecuten los test.

En ambos casos tenemos ‘migrationsRun’ en true para conseguir actualizar la base de datos tras el inicio de la aplicación y ‘migrations’ establece la ruta donde estan nuestras migraciones.

Por ultimo ‘AppDataSource’ proporcionara uno u otro DataSource en función de nuestro entorno.

Recuerda que en el ‘package.json’ es recomendable tener el script de test, de modo que lo podamos ejecutar mas adelante. Ademas de algunos scripts que nos permitirán ejecutar migraciones manualmente.

"scripts": {
"test": "jest",
...
...
"typeorm": "typeorm-ts-node-esm -d ./src/data-source.ts",
"m:gen": "npm run typeorm migration:generate",
"m:run": "npm run typeorm migration:run"
  • m:gen genera las migraciones a partir de las entidades que hemos declarado en DataSource.
  • m:run ejecutara las migraciones por ejemplo en desarrollo o en producción, no lo necesitaremos para el test.

Por ultimo necesitamos iniciar el ‘DataSource’ en Express, para ello añadimos e inicializar ‘AppDataSource’ en nuestro app.ts.

// app.ts

// imports
// ...
import AppDataSource from './data-source';

export const app = express();

AppDataSource.initialize();

// ...
// ...

export default app;

Migraciones TypeORM:

Para crear migraciones necesitamos tener definidas entidades de nuestros modelos de base de datos, para este ejemplo utilizaremos un modelo de usuario básico:

import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
PrimaryColumn,
Index,
} from 'typeorm';

@Entity()
class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column()
lastName!: string;
@Index({ unique: true })
@Column()
userName!: string;
@Index({ unique: true })
@Column()
email!: string;
@Column()
password!: string;
@CreateDateColumn()
created_at!: string;
@UpdateDateColumn()
updated_at!: string;
@DeleteDateColumn()
deleted_at!: string;
}

export default User;

Recuerda que puedes crear entidades con el CLI de TypeORM

typeorm entity:create -n User

Una vez creadas las entidades, ejecutamos el script ‘m:gen’ para generar las migraciones

npm run m:gen -- src/migrations/migrationName

Este comando crea las migraciones en src/migrations, recuerda sustituir ‘migrationName’ por el nombre que tu quieras.

Con esto ya tendríamos todo listo para comenzar con el test.

Test end to end:

Supongamos que tenemos un endpoint ‘singup’ que crea un nuevo usuario.

// controller.ts
// imports....

export const signup = async (req: Request, res: Response) => {
const userRepository = container.resolve(TypeOrmUserRepository);

try {
const request: userDto = {
name: req.body.name,
lastName: req.body.lastName,
userName: req.body.userName,
email: req.body.email,
password: req.body.password,
};

const createUserCommand = new CreateUserCommand(request);

const createUserHandler = new CreateUserHandler(userRepository);
const result = await createUserHandler.run(createUserCommand);
if (result) res.status(200);

res.send('respuesta');
} catch (error) {
res.status(400);
res.send(error);
}
};

En en este controller se inyecta el repository de typeORM a el handler createUserHandler, que posteriormente es invocado enviando un command que no es mas que un DTO con los datos del usuario.

Lo que pretendemos con este test es probar desde la petición http hasta la escritura en la base de datos, a si que vamos a ello.

//controller.test

import request from 'supertest';
import app from '../../../src/app';

describe('POST /api/auth/signup', () => {
const signupPath = '/api/v1/auth/signup';
const newUser = {
name: 'name',
lastName: 'lastname',
userName: 'user1',
email: 'user1@user1.com',
password: '123456',
};

test('Should create user', async () => {
const response = await request(app).post(signupPath).send(newUser);
expect(response.statusCode).toBe(200);
});

test('Should return bad request with empty request', async () => {
const response = await request(app).post(signupPath).send({});
expect(response.statusCode).toBe(400);
});
});

Gracias a Supertest podemos simular una petición http, por lo tanto simulamos un post al que le pasamos el objeto newUser que sería el equivalente a el body de la petición.

En este ejemplo realizamos dos test una para comprobar que el usuario es creado y otro test donde se envía un body vacio esperando un bad request.

El test siempre es mejorable, pero el objetivo es entender como realizar un test end to end.

Conclusión:

Como hemos visto, es relativamente sencillo realizar test end to end con TypeORM como gestor de base de datos ya que nos permite levantar una base de datos en memoria. Esto es interesante si pensamos en realizar integracion continua y despliege continuo, ya que podemos automatizar los test facilmente.

Ten en cuenta que en este test utilizamos una base de datos en memoria con sqlite, es posible que necesites tener los drivers sqlite para tu sistema operativo. Además es posible que por la complejidad de la base de datos no sea suficiente con sqlite en memoria, una posible solución pasa por levantar un docker con una base de datos para test.

--

--

Iván Ramos
0 Followers

I'm software developer passionated aboud good practices and in love with learning of DDD and TDD.