NestJS Integration and E2E Tests with TypeORM, PostGres, and JWT

This guide covers a basic setup for integration and/or end-to-end (e2e) testing on a NestJS project that uses TypeORM and Postgres for database connectivity, and JWT for authentication.

Many of the tips and example tests found in this guide are also applicable to applications that use MongoDB.

This guide assumes you wish to use a live database for testing. Some developers do not feel this step is necessary, while others strongly advocate using a database because it tests an application in a scenario that more closely reflects a production environment.

Regardless of where you stand on the question of a live database, integration and end-to-end tests are an important part of an overall testing/QA strategy.

Unit tests alone fall short in a number of areas. In particular with NestJS there can be a lot of mocking of services, repositories, etc. and extensive mocking runs the risk of masking potential bugs.

The code covered by this guide is available at the following gist:

https://gist.github.com/firxworx/575f019c5ebd67976da164f48c2f4375

Creating a Test Database

Start by setting up a database to use for your tests that is different from your development or production databases.

How you proceed significantly depends on your individual project’s setup so it doesn’t make sense to go into too many details for the purpose of this guide.

One relatively straightforward approach is to use docker to spin up a container running Postgres. I plan on writing another guide in the near-future that covers using docker-compose to spin up a test environment and run e2e tests.

You should also consider how your test database is loaded with fixtures (test data) and reset between test runs. This could be done via separate scripts, or you could can manage the state of your test database within your test code.

I prefer the latter approach so that everything related to spinning up tests is defined alongside the tests themselves.

I provide a tip below to get the app’s TypeORM connection so that you can perform raw operations against your database to help setup and teardown test data.

Environment Variables and Test Database

Next, configure your project to use the test database when it is being run in a test environment.

Ideally your app should be coded to check the values of environment variables and change its behaviour depending on if it is being run in a test, development, and/or production scenario.

One approach is to define different TypeORM configurations that can be selected between based on the value of the NODE_ENV variable. For example if NODE_ENV is ‘development’ or ‘test’, you could supply a particular configuration to TypeORM that is different from ‘production’.

A related approach is to populate the individual values of your TypeORM configuration, such as host, port, username and password from different environment variables, such as DB_HOST, DB_PORT, DB_USER, and DB_PASS.

Recall that you can reference process.env that’s built into NodeJS to access the value of any environment variable. For example, process.env.NODE_ENV can be checked in a conditional statement to see if its value is equal to ‘test’, ‘development’, or ‘production’.

There are many was to work with environment variables and manage configurations in the NodeJS and NestJS ecosystems, so I won’t delve too far into any details because your project is likely to be somewhat unique.

In the scripts section of your package.json file, you can ensure that a given environment variable is set for tests by modifying the command to add an environment variable declaration. For example, the following explicitly sets NODE_ENV=test for tests invoked via the test:e2e script:

"test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json"

This could just as easily be NODE_ENV=development or anything else that happens to suit your particular needs.

Initializing the Application in beforeAll()

The approach I take in this guide starts with the boilerplate test/jest-e2e.json supplied by the NestJS application template.

The boilerplate provides a beforeEach() implementation. For the purposes of this guide, we are going to delete that method because is more straightforward to setup the NestJS application once via the beforeAll() method and then run our tests against it.

The following beforeAll() method initializes our Nest Application based on the root AppModule:

beforeAll(async () => {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile()

  app = moduleFixture.createNestApplication()
  // app.useLogger(new TestLogger()) // more on this line is below
  await app.init()
})

I am assuming a typical setup where your project’s AppModule imports TypeOrmModule.forRoot(...) to provide database connectivity to the rest of your application, and that any modules that use database entities import them via TypeOrmModule.forFeature([ EntityName, ... ]). How to use TypeORM with NestJS is covered in the docs.

Loading Test Data

As mentioned above, you could load your test data via a scripted step. For example, you could have a script defined in the scripts section of package.json that wipes your test database and loads fresh test data.

Another approach (which can also be used in combination with the above approach) is to load a set of representative test data in a beforeAll() method in your test file. You can also use jest’s beforeEach() and afterEach() methods to load/reset data for more granular control before and/or after individual tests.

A good set of test data should flex the various features of your app.

For example, if your app has a notion of active vs. inactive users, or verified/confirmed vs. unverified/unconfirmed users, these should be reflected in your test data and you can then write test cases to confirm that your application behaves as intended for each case.

In terms of loading or deleting data, a useful tip is that you can access the TypeORM database connection used by the app via:

const connection = app.get(Connection)

From the connection you can access the migrations or manager properties or the createQueryBuilder() method as required to perform setup or teardown operations against the database.

For example, with given entityName and data values, you could insert records into the database via:

// raw INSERT query:
await connection.createQueryBuilder().insert().into(entityName).values(data).execute()

// if you need cascades:
await conn.getRepository(entityName).save(data)

Another tip is that you can also easily start from a blank database that is loaded with your schema via:

await connection.synchronize(true)

The true value is for the dropBeforeSync option. This option drops the entire database and all of its data before synchronizing the schema, making it perfect for the start of a test run.

Case Study from Example Project

In one of my projects I have the following block of code in the beforeAll() method that launches the app, immediately following the await app.init() line:

if (process.env.NODE_ENV === 'test') {
    const connection = app.get(Connection)
    await connection.synchronize(true)
    await loadFixtures('data', connection)
}

The code synchronizes the database schema against a fresh database, and the next line runs a loadFixtures() function that I implemented that loads test fixtures (test data) from a yaml file. This approach was inspired by the discussion on typeorm issue #1550.

Note that I specifically check for the ‘test’ environment in case I accidentally run the e2e test script in my local development environment where I want to preserve my working development data.

Closing Down in afterAll()

After all your tests have finished, remember to ensure that your app is closed down with app.close():

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

Example Tests

Suppose our test data includes a valid test user test@example.com with password password.

The following tests a hypothetical auth/login endpoint. It also saves the JWT token received in the response to a higher-scoped variable that we can then reference in subsequent tests:

// assume a higher-scoped variable definition to store the jwt token, e.g.
let jwtToken: string

it('authenticates a user and includes a jwt token in the response', async () => {
  const response = await request(app.getHttpServer())
    .post('/auth/login')
    .send({ email: 'test@example.com', password: 'password' })
    .expect(200)

    // save the access token for subsequent tests
    jwtToken = response.body.accessToken

    // ensure a JWT token is included in the response    
    expect(jwtToken).toMatch(/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/) // jwt regex
})

The next example attempts to login our test@example.com user with an incorrect password. It confirms that the client receives an HTTP 401 response and that it does not include an accessToken:

it('fails to authenticate user with an incorrect password', async () => {
  const response = await request(app.getHttpServer())
    .post('/auth/login')
    .send({ email: 'test@example.com', password: 'wrong' })
    .expect(401)

  expect(response.body.accessToken).not.toBeDefined()
})

We can also confirm our app’s behaviour when an unrecognized user attempts to login. Assume that the user nobody@example.com does not exist in the test database:

it('fails to authenticate user that does not exist', async () => {
  const response = await request(app.getHttpServer())
    .post('/auth/login')
    .send({ email: 'nobody@example.com', password: 'test' })
    .expect(401)

  expect(response.body.accessToken).not.toBeDefined()
})

Since we saved the jwtToken to a higher-scoped variable during the first example test, we can then use that token to make authenticated requests. Supertest includes a set() method where we can set the Authorization header:

it('gets a protected resource with an authenticated request', async () => {
    const response = await request(app.getHttpServer())
      .get('/protected')
      .set('Authorization', `Bearer ${jwtToken}`)
      .expect(200)

    const resources = response.body.data

    // write assertions that reflect your test data scenario
    // e.g. expect(resources).toHaveLength(3) 
})

You can easily modify the above test to send an incorrect or malformed Bearer token and confirm the server response in that case as well.

From here you should have a good foundation to build more tests. Be sure to check the docs for NestJS, jest, and supertest for more details and examples.

If your application is more elaborate and features user roles and permissions, be sure to write tests to confirm that it behaves as intended across different scenarios.

Replacing the Logger

Running tests can generate a lot of log output from your application. One idea to eliminate superfluous log output during tests is to create a LoggerService with mock “no-op” methods and use that as your application logger for tests.

You can add the following class to your project:

class TestLogger implements LoggerService {
  log(message: string) {}
  error(message: string, trace: string) {}
  warn(message: string) {}
  debug(message: string) {}
  verbose(message: string) {}
}

In the above example for beforeAll() there was a commented-out line app.useLogger(new TestLogger()).

With the test logger added to your project and imported into your test file, you could now uncomment this line.

Credit for the mock logger idea goes to Paul Salmon who wrote this post:

https://medium.com/@salmon.3e/integration-testing-with-nestjs-and-typeorm-2ac3f77e7628

Running Tests

The boilerplate NestJS project has defines a test:e2e script in package.json.

In my projects, I modify it to explicitly set the TEST environment variable:

"test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json",

If you use yarn, run the test via yarn test:e2e.

If you are using npm the equivalent is: npm run test:e2e.

Finally, here’s that link again to the gist that contains the code dicussed in this guide:

https://gist.github.com/firxworx/575f019c5ebd67976da164f48c2f4375

Let me know in the comments if this guide helped you with testing your NestJS application :).

Published by

@firxworx

Kevin Firko (@firxworx) is an entrepreneur, developer, and digital project manager based in Toronto, Canada. He works on ventures and client projects through Bitcurve, the independent digital design and development studio he founded in 2008.