Testing NestJS Applications

Writing unit tests, integration tests, and end-to-end tests using Jest and Supertest.


Testing Database Interactions with TypeORM in NestJS

This document explores strategies for effectively testing database interactions within a NestJS application using TypeORM. Testing database interactions directly can be slow and unreliable due to network latency and database state. Therefore, isolating these interactions is crucial for robust and efficient testing.

Why Test Database Interactions?

Testing database interactions is vital for ensuring data integrity, application stability, and overall correctness. It helps verify that your application:

  • Correctly reads and writes data to the database.
  • Handles database errors gracefully.
  • Adheres to defined data constraints and relationships.
  • Works as expected under different data conditions.

Strategies for Testing TypeORM in NestJS

We'll cover two primary strategies for isolating and testing database interactions:

1. Using In-Memory Databases

An in-memory database (like SQLite or PostgreSQL) provides a lightweight, isolated environment for testing. Data is stored in memory, eliminating the need for a persistent database connection and significantly speeding up test execution.

Pros:

  • Speed: Tests run much faster than with a real database.
  • Isolation: Each test suite can have its own isolated database.
  • Reproducibility: Easy to set up and tear down the database state for each test.

Cons:

  • Limited Compatibility: May not perfectly replicate the behavior of your production database (e.g., specific SQL dialects).
  • Requires Setup: Requires configuration to use the in-memory database during testing.

Example: Using SQLite in-memory with TypeORM

First, install the necessary dependencies:

npm install sqlite3 typeorm @nestjs/typeorm --save-dev

Next, configure TypeORM to use SQLite in-memory in your test environment. This can be achieved by modifying your `ormconfig.ts` or environment variables conditionally based on the `NODE_ENV`:

// ormconfig.ts or environment-specific config

  const config = {
    type: 'sqlite',
    database: ':memory:', // Use in-memory database
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true, // Auto-create tables (for testing only!)
    dropSchema: true, // drop schema on each connection (testing only!)
    logging: false,
  };

  module.exports = config; 

Then, in your test setup (e.g., using Jest's `beforeEach` and `afterEach` hooks), establish and close the connection:

// e.g., my.service.spec.ts

  import { Test, TestingModule } from '@nestjs/testing';
  import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
  import { Repository } from 'typeorm';
  import { MyService } from './my.service';
  import { MyEntity } from './my.entity';
  import ormconfig from '../ormconfig'; // Import your TypeORM config

  describe('MyService', () => {
    let service: MyService;
    let repo: Repository<MyEntity>;
    let module: TestingModule;


    beforeEach(async () => {
      module = await Test.createTestingModule({
        imports: [
          TypeOrmModule.forRoot(ormconfig), // Pass your TypeORM Config
          TypeOrmModule.forFeature([MyEntity]),
        ],
        providers: [MyService],
      }).compile();

      service = module.get<MyService>(MyService);
      repo = module.get<Repository<MyEntity>>(getRepositoryToken(MyEntity));

      // Optional: Seed the database with test data here
      await repo.clear(); // Clear the database before each test
      await repo.save({ name: 'Test Item' });

    });

    afterEach(async () => {
        await module.close(); // important to close the database connection so the tests run with fresh data
    });

    it('should be defined', () => {
      expect(service).toBeDefined();
    });

    it('should find an entity', async () => {
      const entity = await service.findOne('Test Item');
      expect(entity).toBeDefined();
      expect(entity.name).toEqual('Test Item');
    });
  }); 

2. Mocking Repositories

Mocking repositories involves creating mock implementations of TypeORM's `Repository` class. This allows you to control the data returned and verify that the service interacts with the repository as expected, without actually touching the database.

Pros:

  • Complete Isolation: No database interaction whatsoever.
  • Fine-grained Control: You can precisely define the behavior of the mock repository.
  • Fastest Tests: The fastest option, as there's no database overhead.

Cons:

  • More Complex Setup: Requires more effort to create and maintain the mock repositories.
  • Risk of Inaccuracy: If the mock doesn't accurately reflect the real repository's behavior, the tests may not be reliable.

Example: Mocking with Jest

// my.service.spec.ts (using Jest)

  import { Test, TestingModule } from '@nestjs/testing';
  import { getRepositoryToken } from '@nestjs/typeorm';
  import { Repository } from 'typeorm';
  import { MyService } from './my.service';
  import { MyEntity } from './my.entity';

  describe('MyService', () => {
    let service: MyService;
    const mockRepository = {
        findOne: jest.fn(),
        save: jest.fn(),
        delete: jest.fn()
    };

    beforeEach(async () => {
      const module: TestingModule = await Test.createTestingModule({
        providers: [
          MyService,
          {
            provide: getRepositoryToken(MyEntity),
            useValue: mockRepository, // Use the mock repository
          },
        ],
      }).compile();

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

    it('should be defined', () => {
      expect(service).toBeDefined();
    });

    it('should find an entity by name', async () => {
      const expectedEntity = { id: 1, name: 'Mock Entity' };
      mockRepository.findOne.mockReturnValue(Promise.resolve(expectedEntity));

      const entity = await service.findOne('Mock Entity');

      expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { name: 'Mock Entity' } }); //verify method call with correct parameters.
      expect(entity).toEqual(expectedEntity);
    });
  }); 

Choosing the Right Strategy

The best strategy depends on your specific needs:

  • For fast unit tests that primarily focus on logic, mocking is ideal.
  • For integration tests that verify the interaction between your service and the database (but still want isolation), in-memory databases are a good choice.
  • End to end testing may benefit from using real databases, or database containers.

Conclusion

Testing database interactions in NestJS applications using TypeORM is crucial for building reliable and robust software. By employing strategies like in-memory databases and mocking repositories, you can create isolated, fast, and reproducible tests that ensure the correctness of your data access layer.