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
↩︎