Tom Nurkkala
Tom Nurkkala

Reputation: 652

Test NestJS Service against Actual Database

I would like to be able to test my Nest service against an actual database. I understand that most unit tests should use a mock object, but it also, at times, makes sense to test against the database itself.

I have searched through SO and the GH issues for Nest, and am starting to reach the transitive closure of all answers. :-)

I am trying to work from https://github.com/nestjs/nest/issues/363#issuecomment-360105413. Following is my Unit test, which uses a custom provider to pass the repository to my service class.

describe("DepartmentService", () => {
  const token = getRepositoryToken(Department);
  let service: DepartmentService;
  let repo: Repository<Department>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        DepartmentService,
        {
          provide: token,
          useClass: Repository
        }
      ]
    }).compile();

    service = module.get<DepartmentService>(DepartmentService);
    repo = module.get(token);
  });

Everything compiles properly, TypeScript seems happy. However, when I try to execute create or save on my Repository instance, the underlying Repository appears to be undefined. Here's the stack backtrace:

TypeError: Cannot read property 'create' of undefined

  at Repository.Object.<anonymous>.Repository.create (repository/Repository.ts:99:29)
  at DepartmentService.<anonymous> (relational/department/department.service.ts:46:53)
  at relational/department/department.service.ts:19:71
  at Object.<anonymous>.__awaiter (relational/department/department.service.ts:15:12)
  at DepartmentService.addDepartment (relational/department/department.service.ts:56:16)
  at Object.<anonymous> (relational/department/test/department.service.spec.ts:46:35)
  at relational/department/test/department.service.spec.ts:7:71

It appears that the EntityManager instance with the TypeORM Repository class is not being initialized; it is the undefined reference that this backtrace is complaining about.

How do I get the Repository and EntityManager to initialize properly?

thanks, tom.

Upvotes: 20

Views: 33025

Answers (6)

zemil
zemil

Reputation: 5066

In our team we prefer "real life" testing without mocking databases.

So I used existing AppModule for testing:

import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

import { AppModule } from '../../../app.module';
import { AuthService } from '../auth.service';
import { UserRepo } from '../../user/user.repo';

describe('AuthService', () => {
    let app: INestApplication;
    let service: AuthService;
    let repo: UserRepo;

    beforeAll(async () => {
        app = await NestFactory.create(AppModule);
        service = app.get(AuthService);

        repo = app.get(UserRepo); // or with private access service['userRepo'];
    });

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

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

    describe('login', () => {
        it('should login user', async () => {
            const user = await service.login({ email: '[email protected]', password: '12345678' });

            expect(user.id).toBeDefined();
        });
    });

    describe('userRepo', () => {
        it('should find user without errors', async () => {
            const user = await repo.findBy({ email: '[email protected]' });

            expect(user).toBeDefined();
        });

        it('should throw not found exception', async () => {    
            await expect(repo.findBy({ email: '[email protected]' })).rejects.toThrow();
        });
    });
});

Also to add env variables in your configuration file or AppModule:

import { config as testConfig } from 'dotenv';

if (process.env.NODE_ENV === 'test') {
    testConfig({ path: resolve('./.test.env') });
}

Upvotes: 1

Amir Molaei
Amir Molaei

Reputation: 3810

I usually import AppModule for database connection, and finally after tests are executed I close the connection:

  let service: SampleService;
  let connection: Connection;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [AppModule, TypeOrmModule.forFeature([SampleEntity])],
      providers: [SampleService],
    }).compile();

    service = module.get<SampleService>(SampleService);
    connection = await module.get(getConnectionToken());
  });

  afterEach(async () => {
    await connection.close();
  });

Upvotes: 1

andersnylund
andersnylund

Reputation: 190

I created a test orm configuration

// ../test/db.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { EntitySchema } from 'typeorm';

type Entity = Function | string | EntitySchema<any>;

export const createTestConfiguration = (
  entities: Entity[],
): TypeOrmModuleOptions => ({
  type: 'sqlite',
  database: ':memory:',
  entities,
  dropSchema: true,
  synchronize: true,
  logging: false,
});

which I then utilize when setting up the tests

// books.service.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import { HttpModule, HttpService } from '@nestjs/common';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { BooksService } from './books.service';
import { Book } from './book.entity';
import { createTestConfiguration } from '../../test/db';

describe('BooksService', () => {
  let module: TestingModule;
  let service: BooksService;
  let httpService: HttpService;
  let repository: Repository<Book>;

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [
        HttpModule,
        TypeOrmModule.forRoot(createTestConfiguration([Book])),
        TypeOrmModule.forFeature([Book]),
      ],
      providers: [BooksService],
    }).compile();

    httpService = module.get<HttpService>(HttpService);
    service = module.get<BooksService>(BooksService);
    repository = module.get<Repository<Book>>(getRepositoryToken(Book));
  });

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

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

This allows to query the repository after the tests and ensure that the correct data was inserted.

Upvotes: 1

kenberkeley
kenberkeley

Reputation: 9766

I prefer not using @nestjs/testing for the sake of simplicity.

First of all, create a reusable helper:

/* src/utils/testing-helpers/createMemDB.js */
import { createConnection, EntitySchema } from 'typeorm'
type Entity = Function | string | EntitySchema<any>

export async function createMemDB(entities: Entity[]) {
  return createConnection({
    // name, // let TypeORM manage the connections
    type: 'sqlite',
    database: ':memory:',
    entities,
    dropSchema: true,
    synchronize: true,
    logging: false
  })
}

Then, write test:

/* src/user/user.service.spec.ts */
import { Connection, Repository } from 'typeorm'
import { createMemDB } from '../utils/testing-helpers/createMemDB'
import UserService from './user.service'
import User from './user.entity'

describe('User Service', () => {
  let db: Connection
  let userService: UserService
  let userRepository: Repository<User>

  beforeAll(async () => {
    db = await createMemDB([User])
    userRepository = await db.getRepository(User)
    userService = new UserService(userRepository) // <--- manually inject
  })
  afterAll(() => db.close())

  it('should create a new user', async () => {
    const username = 'HelloWorld'
    const password = 'password'

    const newUser = await userService.createUser({ username, password })
    expect(newUser.id).toBeDefined()

    const newUserInDB = await userRepository.findOne(newUser.id)
    expect(newUserInDB.username).toBe(username)
  })
})

Refer to https://github.com/typeorm/typeorm/issues/1267#issuecomment-483775861

Upvotes: 14

Tom Nurkkala
Tom Nurkkala

Reputation: 652

Here's an update to the test that employs Kim Kern's suggestion.

describe("DepartmentService", () => {
  let service: DepartmentService;
  let repo: Repository<Department>;
  let module: TestingModule;

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot(),
        TypeOrmModule.forFeature([Department])
      ],
      providers: [DepartmentService]
    }).compile();

    service = module.get<DepartmentService>(DepartmentService);
    repo = module.get<Repository<Department>>(getRepositoryToken(Department));
  });

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

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

  // ...
}

Upvotes: 12

Kim Kern
Kim Kern

Reputation: 60357

To initialize typeorm properly, you should just be able to import the TypeOrmModule in your test:

Test.createTestingModule({
  imports: [
   TypeOrmModule.forRoot({
        type: 'mysql',
        // ...
   }),
   TypeOrmModule.forFeature([Department])
  ]

Upvotes: 21

Related Questions