Search code examples
typescriptunit-testingnestjsnestjs-typeorm

Using transactions with NestJS & TypeORM


I want to write such a unit test file that each time I run it, I run an instance of the entire application. This is ensured by this part:

module = await Test.createTestingModule({
  imports: [AppModule],
}).compile();

Then I wanted to clean up the database after each unit test. ChatGPT led me to the conclusion that I should use transactions. It suggested that I should use:

beforeEach(async () => {
  await dataSource.query('BEGIN');
});

afterEach(async () => {
  await dataSource.query('ROLLBACK');
});

But it didn't want to work for me.

Then after some chatting with the chat, I came to the conclusion that I should use queryRunner.startTransaction() and queryRunner.rollbackTransaction(). But it still doesn't want to work for me.

Please help me fix the below code.

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { AppModule } from '../../app.module';
import { DataSource, QueryRunner } from 'typeorm';
import { DATA_SOURCE } from '../database/database.providers';
import { runSeeders } from 'typeorm-extension';
import { ConfigService } from '@nestjs/config';

describe('UsersService', () => {
  let service: UsersService;
  let module: TestingModule;
  let dataSource: DataSource;
  let queryRunner: QueryRunner;
  let config: ConfigService;
  let createQueryRunner;
  let release;

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    config = module.get<ConfigService>(ConfigService);
    dataSource = module.get<DataSource>(DATA_SOURCE);

    if (config.get<string>('NODE_ENV') === 'testing') {
      console.info('Running seeders for testing environment');
      await runSeeders(dataSource);
    }

    service = module.get<UsersService>(UsersService);
  });

  beforeEach(async () => {
    queryRunner = dataSource.createQueryRunner();
    await queryRunner.startTransaction();
  });

  afterEach(async () => {
    await queryRunner.rollbackTransaction();

    await queryRunner.release();
  });

  afterAll(async () => {
    await module.close();
  });

  it('should be defined', async () => {
    await service.create({ firstName: 'FN', lastName: 'LN', email: '[email protected]' });
    await service.create({ firstName: 'FN2', lastName: 'LN2', email: '[email protected]' });

    expect(service).toBeDefined();
  });

  it('should return a user', async () => {
    const user = await service.findOneByEmail('[email protected]');
    expect(user).toBeDefined();
  });
});

Solution

  • Through some trial and error, I came to this solution. I used the <<== comments to highlight lines that were added to the previous example.

    import { Test, TestingModule } from '@nestjs/testing';
    import { UsersService } from './users.service';
    import { AppModule } from '../../app.module';
    import { DataSource, QueryRunner } from 'typeorm';
    import { DATA_SOURCE } from '../database/database.providers';
    import { runSeeders } from 'typeorm-extension';
    import { ConfigService } from '@nestjs/config';
    
    describe('UsersService', () => {
      let service: UsersService;
      let module: TestingModule;
      let dataSource: DataSource;
      let queryRunner: QueryRunner;
      let config: ConfigService;
      let createQueryRunner;
      let release;
    
      beforeAll(async () => {
        module = await Test.createTestingModule({
          imports: [AppModule],
        }).compile();
    
        config = module.get<ConfigService>(ConfigService);
        dataSource = module.get<DataSource>(DATA_SOURCE);
    
        if (config.get<string>('NODE_ENV') === 'testing') {
          console.info('Running seeders for testing environment');
          await runSeeders(dataSource);
        }
    
        service = module.get<UsersService>(UsersService);
      });
    
      beforeEach(async () => {
        queryRunner = dataSource.createQueryRunner();
        await queryRunner.startTransaction();
    
        createQueryRunner = dataSource.createQueryRunner; // <<==
        release = queryRunner.release;                    // <<==
    
        dataSource.createQueryRunner = () => queryRunner; // <<==
        queryRunner.release = () => Promise.resolve();    // <<==
      });
    
      afterEach(async () => {
        await queryRunner.rollbackTransaction();
    
        dataSource.createQueryRunner = createQueryRunner; // <<==
        queryRunner.release = release;                    // <<==
    
        await queryRunner.release();
      });
    
      afterAll(async () => {
        await module.close();
      });
    
      it('should be defined', async () => {
        await service.create({ firstName: 'FN', lastName: 'LN', email: '[email protected]' });
        await service.create({ firstName: 'FN2', lastName: 'LN2', email: '[email protected]' });
    
        expect(service).toBeDefined();
      });
    
      it('should return a user', async () => {
        const user = await service.findOneByEmail('[email protected]');
        expect(user).toBeDefined();
      });
    });
    

    Explanation:

    I suppose that almost every time a query to the database is made we use dataSource.createQueryRunner().query(<SOME SQL QUERY>), which creates a new queryRunner. But for transactions to work for us in our tests file we must use the same queryRunner each time a query is made. Above is shown how I figured out how to enforce this behavior. If anyone has a better solution, please feel free to share it. I hope that this problem in TypeORM will be fixed sometime soon, or some better workaround will be introduced.