NestJS projects that use TypeORM with MongoDB are rare. The recommended ODM1 to use with MongoDB is Mongoose, but the choice is not always within our control. This article is a guide for writing integration tests in a NestJS project that uses TypeORM with MongoDB.
Integration tests verify that all units required to accomplish a particular goal work together as expected. For integration tests in a backend application, database calls are not mocked. API calls to external services such as Slack or Stripe may be mocked.
The NestJS Sample Project
The NestJS sample project for this article can be found at orimdominic/nest-mongo-typeorm-e2e. The project database has a collection of users and items. Each user can have a list of items saved to their account.
There are endpoints to
- create a user
POST /users
, - create a user’s item
POST /items
, - get items
GET /items
and, - get an item
GET /items/:id
.
All these endpoints and the functionaity surrounding them have been created. All we want to do is write integration tests for them.
Setting Up The Project
To set up a basic NestJS project,
- install the NestJS CLI globally using your package manager
npm i -g @nestjs/cli
oryarn global add @nestjs/cli
- create a new project with
nest new project-name
Other libraries to install include
# within the project directory's terminal
npm install mongodb@3.7.0 typeorm @nestjs/typeorm mongodb-memory-server
Setting Up The Tests
We want to use two databases; mongodb-memory-server
for integration tests
and mongodb
for running the application. In app.module.ts
,
we set up the TypeORM-mongodb connection to the database for running the application.
/**
* @file app.module.ts
*/
//...other imports
@Module({
imports: [
TypeOrmModule.forRoot({
url: 'mongodb://127.0.0.1:27017/mongo-typeorm-e2e',
type: 'mongodb',
entities: [join(__dirname + '/**/*.entity{.ts,.js}')],
}),
// ...
],
//...
})
Testing UsersController
We create a spec file with the name users.e2e-spec.ts
. The file name must end in
e2e-spec.ts
because that is the filter that NestJS uses to find integration/end-to-end
test files. The users.e2e-spec.ts
file will contain tests for API calls
to routes handled by UsersController.
We have only one endpoint here - POST /users
which is used to create users.
In setting up the test for the endpoint, before any test is run, we
- create an instance of
mongo-memory-server
- create a fixture module to boostrap the application and setup its endpoints for receiving requests
- override the
TypeOrmModuleOptions
token so that we can replace the database connection with that of our mongo-memory-server instance 3 - create the application from the fixture module
- intialise the the application
- get the tokens that we need from the application for the module that is under test
/**
* @file users.e2e-spec.ts
*/
describe("UsersController (e2e)", () => {
let app: INestApplication,
mongod: MongoMemoryServer,
usersRepo: MongoRepository<User>;
beforeAll(async () => {
mongod = await MongoMemoryServer.create(); // 1
const moduleFixture: TestingModule = await Test.createTestingModule({
// 2
imports: [AppModule],
})
.overrideProvider("TypeOrmModuleOptions") // 3
.useValue({
url: mongod.getUri(),
type: "mongodb",
entities: [join(__dirname + "/**/*.entity{.ts,.js}")],
})
.compile();
app = moduleFixture.createNestApplication(); // 4
await app.init(); // 5
usersRepo = moduleFixture.get(getRepositoryToken(User)); // 6
});
});
To test the POST /users
route, we include the following test suite in the file,
on the same level as the beforeAll
block.
describe("POST /users", async () => {
afterEach(async () => {
await usersRepo.deleteMany({}); // 1
});
it("creates a new user", () => {
return request(app.getHttpServer())
.post("/users")
.send({
name: "Demo User",
email: "demo.user@mail.com",
})
.expect(201)
.expect((res) => {
expect(res.body).toMatchObject({
name: "Demo User",
email: "demo.user@mail.com",
_id: expect.any(String),
});
});
});
});
At 1
, we want to clear all data in the users collection after each test so that
each test can start with an empty collection of users. The creates a new user
test block contains the test to verify that a new user can be successfully created
by the POST /users
endpoint if the right payload is passed to it.
At this point, running yarn test:e2e users
will work, but there will be issues.
The test suite will not exit when done because the Nest application was not shut
down. To shut it down, we introduce the afterAll
block on the same level as the
beforeAll
block, with the command to shut down the application for this test suite.
app.close()
explicitly shuts down the application. We also stop the mongod
running instance.
afterAll(async () => {
await mongod.stop();
await app.close();
});
Running yarn test:e2e users
should work as expected now.
Testing ItemsController
The same procedure used intesting UsersController
is used in testing ItemsController
.
An e2e test file is created - items.e2e-spec.ts
, a database instance of the
mongo-memory-server is created and is used to override the application’s database
connection. An application for the test is bootstrapped and the necessary tokens
are retrieved. The application is closed after all tests have run.
Note: You may see the error
[ExceptionHandler] Cannot execute operation on "default" connection because connection is not yet established.
on your console. This occurs before the items controller test is is run. I have
debugged this but I couldn’t find the source. If you find a fix or its source,
please reach out to me on Twitter or
LinkedIn.
[Software code testing Cartoon Illustrations
- Image Work illustrations by Storyset](https://storyset.com/work)
ODM: Object Document Mapper. A library that maps data in a programming language to documents in a database. ↩︎
TypeORM does not support mongdb drivers > v3 at the time of writing. Take a look at this issue: Add support for mongodb driver v4 ↩︎
Many thanks to Jay McDoniel for his input on overriding the database provider in the tests instead of in a conditional in
app.module.ts
↩︎