Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using transactions in tests w/ NestJS & TypeORM #10874

Open
1 of 18 tasks
akucintavalent opened this issue May 3, 2024 · 0 comments
Open
1 of 18 tasks

Using transactions in tests w/ NestJS & TypeORM #10874

akucintavalent opened this issue May 3, 2024 · 0 comments

Comments

@akucintavalent
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant