Reputation: 652
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
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
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
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
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
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
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