Skip to content

Using transactions in tests w/ NestJS & TypeORM #10874

Open
@akucintavalent

Description

@akucintavalent

Issue description

Transactions don't work in tests

Expected Behavior

I posted an issue on stackoverflow:

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: 'bogdan@desmart.com' });
    await service.create({ firstName: 'FN2', lastName: 'LN2', email: 'bogdan2@desmart.com' });

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

  it('should return a user', async () => {
    const user = await service.findOneByEmail('bogdan@desmart.com');
    expect(user).toBeDefined();
  });
});

Actual Behavior

It doesn't work.

Steps to reproduce

Run the above code and see that it doesn't work.

My Environment

| Dependency | Version |
| Operating System | Alpine Linux v3.19.1 Docker container |
| Node.js version | 18.20.2 |
| Typescript version | 5.3.3 |
| TypeORM version | typeorm@npm:0.3.19 |
| nestjs/typeorm | @nestjs/typeorm@npm:10.0.1 |

Additional Context

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: 'bogdan@desmart.com' });
    await service.create({ firstName: 'FN2', lastName: 'LN2', email: 'bogdan2@desmart.com' });

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

  it('should return a user', async () => {
    const user = await service.findOneByEmail('bogdan@desmart.com');
    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.

Relevant Database Driver(s)

  • aurora-mysql
  • aurora-postgres
  • better-sqlite3
  • cockroachdb
  • cordova
  • expo
  • mongodb
  • mysql
  • nativescript
  • oracle
  • postgres
  • react-native
  • sap
  • spanner
  • sqlite
  • sqlite-abstract
  • sqljs
  • sqlserver

Are you willing to resolve this issue by submitting a Pull Request?

No, I don’t have the time and I’m okay to wait for the community / maintainers to resolve this issue.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions