Testing entire flows

It's time to finally implement our more realistic, e2e tests.

Now we can focus entirely on the test file. Back to it, first let's replace beforeEach() with beforeAll(), so that the app is initialized just once instead of before each test. Let's also add an afterAll() call at the bottom to clean up resources, such as close the database connection, etc.

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

Here, the 5 routes will be hit sequentially in the same database. Ergo, it will be verified if when:

  • Creating a user, it is created successfully

  • Searching for all users, there is a single one

  • Searching for this user, it is found

  • Updating this user, the field is changed

  • Removing this user, it no longer exists

To help us, let's put at the top of the file:

  • A fake createUserDto

  • A fake updateUserDto

  • An expectedUser

const createUserDto: CreateUserDto = {
  name: faker.person.firstName(),
  email: faker.internet.email(),
  phone: faker.phone.number(),
  password: 'AAaa11!!',
};

const updateUserDto: UpdateUserDto = {
  ...createUserDto,
  name: faker.person.firstName(),
};

const expectedUser = expect.objectContaining({
  id: 1,
  ...createUserDto,
  role: Role.User,
});

Note the following:

  • The password is hardcoded due to its validation rules

  • The updateUserDto receives the contents of thecreateUserDto due to a problem when using Mapped Types from Swagger with the request from the supertest library

  • We use objectContaining() to check only some of the expectedUser fields

Now, let's change the name of the describe() to the resource being tested and its base path.

'Users [/users]'

In the imports of the module, let's add the ConfigModule so we can access the environment variables, and use the validation schema for testing. We'll also connect with the database right after it.

ConfigModule.forRoot({
  expandVariables: true,
  validationSchema: TEST_ENV_VALIDATION_SCHEMA,
}),
TypeOrmModule.forRootAsync(testDatabaseConfig.asProvider()),

And lastly, in the providers, enable the ValidationPipe and the NotFoundExceptionFilter globally.

{
  provide: APP_PIPE,
  useValue: new ValidationPipe(VALIDATION_PIPE_OPTIONS),
},
{
  provide: APP_FILTER,
  useClass: NotFoundExceptionFilter,
},

Now, remember that writing style we used in that default test? Let's then continue to use it and everything will be much simpler.

Beginning with the test for create(), we shall

  • Access the /users route with POST

    • While sending the createUserDto in the body with send()

  • Check if

    • The status is CREATED

    • The body matches the expectedUser

it('Create [POST /]', async () => {
  const response = await request(server).post('/users').send(createUserDto);
  const { status, body } = response;

  expect(status).toBe(HttpStatus.CREATED);
  expect(body).toEqual(expectedUser);
});

In findAll(), we

  • Access the /users route with GET

  • Check if

    • The status is OK

    • The body contains a single element

    • It matches the expectedUser

it('Find all [GET /]', async () => {
  const response = await request(server).get('/users');
  const { status, body } = response;

  expect(status).toBe(HttpStatus.OK);
  expect(body.length).toBe(1);
  expect(body[0]).toEqual(expectedUser);
});

In findOne(), there's nothing new.

it('Find one [GET /:id]', async () => {
  const response = await request(server).get('/users/1');
  const { status, body } = response;

  expect(status).toBe(HttpStatus.OK);
  expect(body).toEqual(expectedUser);
});

In update(), we just check if the name was altered. The findOne() is also used for checking this.

it('Update [PATCH /:id]', async () => {
  const response = await request(server)
    .patch('/users/1')
    .send(updateUserDto);
  const { status, body } = response;

  expect(status).toBe(HttpStatus.OK);
  expect(body.name).toBe(updateUserDto.name);

  const findOneResponse = await request(server).get('/users/1');
  expect(findOneResponse.body.name).toBe(updateUserDto.name);
});

Finally, in remove(), we also check if a findOne() call will result in the status NOT FOUND.

it('Remove [DELETE /:id]', async () => {
  const response = await request(server).delete('/users/1');
  const { status } = response;

  expect(status).toBe(HttpStatus.OK);

  const findOneResponse = await request(server).get('/users/1');
  expect(findOneResponse.status).toBe(HttpStatus.NOT_FOUND);
});

And we're done! Just remember to delete that default test and then run the e2e tests.

yarn test:e2e

Commit - Implementing e2e tests

Last updated