Lastly, for completeness, here are the end to end tests for our application endpoints. In case of any issues, you can refer to my Github repository here.

Main app e2e tests

test/app.e2e-spec.ts view raw
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    jest.setTimeout(30 * 1000);

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

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });

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

The main app e2e test mainly has some boilerplate code that comes by default when we generate our application.

When you run the tests they should pass:

npm run test:e2e app

Output:

> storefront-backend@0.0.1 test:e2e
> jest --config ./test/jest-e2e.json "app"

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 0ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

 PASS  test/app.e2e-spec.ts
  AppController (e2e)
    ✓ / (GET) (34 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.004 s
Ran all test suites matching /app/i.

Auth e2e tests

We test our auth endpoints here. The describe blocks explain what the specs are meant to do:

test/auth.e2e-spec.ts view raw
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { Customer } from '../src/modules/customers/entities/customer.entity';
import { faker } from '@faker-js/faker';
import * as cookieParser from 'cookie-parser';

describe('AuthController (e2e)', () => {
  let app: INestApplication;
  const testPassword = faker.random.alpha(10);
  const testFirstName = faker.name.firstName();
  const testLastName = faker.name.lastName();
  const fakeCustomer = new Customer({
    name: testFirstName + ' ' + testLastName,
    email: `${testFirstName.toLowerCase()}@example.com`,
    phoneNumber: faker.phone.imei(),
    password: testPassword,
    confirmPassword: testPassword,
  });

  beforeAll(async () => {
    jest.setTimeout(30 * 1000);

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

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe());
    app.use(cookieParser('testString'));
    await app.init();
  });

  describe('when registering', () => {
    describe('and using valid data', () => {
      it('should respond with the customer data minus the password', () => {
        return request(app.getHttpServer())
          .post('/auth/register')
          .send({
            email: fakeCustomer.email,
            name: fakeCustomer.name,
            phoneNumber: fakeCustomer.phoneNumber,
            password: fakeCustomer.password,
            confirmPassword: fakeCustomer.confirmPassword,
          })
          .expect(201);
      });

      it('should throw an error when a duplicate user is registered', () => {
        return request(app.getHttpServer())
          .post('/auth/register')
          .send({
            email: fakeCustomer.email,
            name: fakeCustomer.name,
            phoneNumber: fakeCustomer.phoneNumber,
            password: fakeCustomer.password,
            confirmPassword: fakeCustomer.confirmPassword,
          })
          .expect(400);
      });
    });

    describe('and using invalid data', () => {
      it('should throw an error', () => {
        return request(app.getHttpServer())
          .post('/auth/register')
          .send({
            name: fakeCustomer.name,
          })
          .expect(400);
      });
    });
  });

  describe('when logging in', () => {
    describe('and using valid data', () => {
      it('should respond with a success message', () => {
        return request(app.getHttpServer())
          .post('/auth/login')
          .send({
            email: fakeCustomer.email,
            password: fakeCustomer.password,
          })
          .expect(201)
          .expect({
            msg: 'success',
          });
      });
    });

    describe('and using invalid data', () => {
      it('should respond with a bad request error message', () => {
        return request(app.getHttpServer())
          .post('/auth/login')
          .send({
            email: 'fake@fake.com',
            password: 'fakepassword',
          })
          .expect(400)
          .expect({
            statusCode: 400,
            message: 'Wrong credentials provided',
            error: 'Bad Request',
          });
      });
    });
  });

  describe('when logging out', () => {
    describe('and using an invalid cookie', () => {
      it('should respond with an unauthorized message', () => {
        return request(app.getHttpServer()).delete('/auth/log_out').expect(401);
      });
    });
  });

  describe('when getting a refresh token', () => {
    describe('without a valid jwt token', () => {
      it('should respond with an unauthorized message', () => {
        return request(app.getHttpServer())
          .get('/auth/refresh_token')
          .expect(401);
      });
    });
  });

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

These should also pass when run:

npm run test:e2e auth

Output:

> storefront-backend@0.0.1 test:e2e
> jest --config ./test/jest-e2e.json "auth"

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 64ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 58ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

 PASS  test/auth.e2e-spec.ts
  AuthController (e2e)
    when registering
      and using valid data
        ✓ should respond with the customer data minus the password (103 ms)
        ✓ should throw an error when a duplicate user is registered (55 ms)
      and using invalid data
        ✓ should throw an error (3 ms)
    when logging in
      and using valid data
        ✓ should respond with a success message (116 ms)
      and using invalid data
        ✓ should respond with a bad request error message (3 ms)
    when logging out
      and using an invalid cookie
        ✓ should respond with an unauthorized message (2 ms)
    when getting a refresh token
      without a valid jwt token
        ✓ should respond with an unauthorized message (2 ms)

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        4.869 s
Ran all test suites matching /auth/i.

Orders e2e tests

Here we start by mocking an auth guard as well as the expected results before calling the order controller endpoints:

test/orders.e2e-spec.ts view raw
import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate, INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { JwtAuthGuard } from '../src/modules/auth/guards/jwt-auth.guard';
import * as cookieParser from 'cookie-parser';
import { OrdersService } from '../src/modules/orders/orders.service';

describe('OrdersController (e2e)', () => {
  let app: INestApplication;
  const mockAuthGuard: CanActivate = { canActivate: () => true };
  const mockOrdersService = {
    create: () => {
      return {
        customerId: '1660be29-204e-4bfa-a68e-23ecb042d8c3',
        totalAmount: 55.25,
        orderItems: [
          {
            productId: '2f3b4332-1189-4a95-aed1-d8a243e7bacb',
            quantity: 2,
            unitPrice: 12.13,
            orderId: '374eb139-cd27-4829-aeb1-220844fc0414',
          },
          {
            productId: '43efaa10-ffc2-4c24-a05b-5aef6237b5c1',
            quantity: 3,
            unitPrice: 10.33,
            orderId: '374eb139-cd27-4829-aeb1-220844fc0414',
          },
        ],
        id: '374eb139-cd27-4829-aeb1-220844fc0413',
        paymentStatus: 'Created',
        createdAt: '2022-06-07T14:10:49.101Z',
        updatedAt: '2022-06-07T14:10:49.101Z',
        clientSecret: 'pi_test_client_secret',
      };
    },
    updatePaymentStatus: () => {
      return { message: 'success' };
    },
  };

  beforeAll(async () => {
    jest.setTimeout(30 * 1000);

    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideGuard(JwtAuthGuard)
      .useValue(mockAuthGuard)
      .overrideProvider(OrdersService)
      .useValue(mockOrdersService)
      .compile();

    app = moduleFixture.createNestApplication();
    app.use(cookieParser('testString'));
    await app.init();
  });

  describe('order creation', () => {
    it('returns an order object', () => {
      return request(app.getHttpServer())
        .post('/orders')
        .expect(201)
        .expect(mockOrdersService.create());
    });
  });

  describe('stripe callback', () => {
    it('updates the order payment status via the webhook', () => {
      return request(app.getHttpServer())
        .post('/orders/stripe_webhook')
        .expect(201)
        .expect(mockOrdersService.updatePaymentStatus());
    });
  });

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

Run the spec:

npm run test:e2e orders

Output:

> storefront-backend@0.0.1 test:e2e
> jest --config ./test/jest-e2e.json "orders"

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 1ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 0ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

 PASS  test/orders.e2e-spec.ts
  OrdersController (e2e)
    order creation
      ✓ returns an order object (34 ms)
    stripe callback
      ✓ updates the order payment status via the webhook (4 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.638 s
Ran all test suites matching /orders/i.

Products e2e tests

Lastly we test our products controller endpoints, using mocks where necessary:

test/products.e2e-spec.ts view raw
import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate, INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { JwtAuthGuard } from '../src/modules/auth/guards/jwt-auth.guard';
import * as cookieParser from 'cookie-parser';
import { ProductsService } from '../src/modules/products/products.service';
import { DeleteResult, UpdateResult } from 'typeorm';

describe('ProductsController (e2e)', () => {
  let app: INestApplication;
  const mockAuthGuard: CanActivate = { canActivate: () => true };

  const mockProductObject = {
    name: 'Test Product 03',
    unitPrice: 10.13,
    description: 'Test Product 03',
    image: 'images/',
    deletedAt: null,
    id: '10335531-df60-4c24-9597-8ce13d841929',
    createdAt: '2022-06-08T13:57:28.247Z',
    updatedAt: '2022-06-08T13:57:28.247Z',
  };

  const mockTypeormResult: UpdateResult | DeleteResult = {
    generatedMaps: [],
    raw: [],
    affected: 1,
  };

  const mockProductsService = {
    create: () => {
      return mockProductObject;
    },
    findOne: () => {
      return mockProductObject;
    },
    findAll: () => {
      return [mockProductObject, mockProductObject];
    },
    update: () => {
      return mockTypeormResult;
    },
    remove: () => {
      return mockTypeormResult;
    },
  };

  beforeAll(async () => {
    jest.setTimeout(30 * 1000);

    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideGuard(JwtAuthGuard)
      .useValue(mockAuthGuard)
      .overrideProvider(ProductsService)
      .useValue(mockProductsService)
      .compile();

    app = moduleFixture.createNestApplication();
    app.use(cookieParser('testString'));
    await app.init();
  });

  describe('product creation', () => {
    it('returns a product object', () => {
      return request(app.getHttpServer())
        .post('/products')
        .expect(201)
        .expect(mockProductsService.create());
    });
  });

  describe('find all products', () => {
    it('returns an array of products', () => {
      return request(app.getHttpServer())
        .get('/products')
        .expect(200)
        .expect(mockProductsService.findAll());
    });
  });

  describe('updating a product', () => {
    it('returns an update result', () => {
      return request(app.getHttpServer())
        .patch(`/products/${mockProductObject.id}`)
        .send({ name: 'Berries' })
        .expect(200)
        .expect(mockProductsService.update());
    });
  });

  describe('deleting a product', () => {
    it('returns a delete result', () => {
      return request(app.getHttpServer())
        .delete(`/products/${mockProductObject.id}`)
        .expect(200)
        .expect(mockProductsService.remove());
    });
  });

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

Run the spec:

npm run test:e2e products

Output:

> storefront-backend@0.0.1 test:e2e
> jest --config ./test/jest-e2e.json "products"

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 0ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 1ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 0ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

  console.log
    Before...

      at LoggingInterceptor.intercept (../src/common/interceptors/logging.interceptor.ts:13:13)

  console.log
    After... 0ms

      at Object.next (../src/common/interceptors/logging.interceptor.ts:18:31)

 PASS  test/products.e2e-spec.ts
  ProductsController (e2e)
    product creation
      ✓ returns a product object (98 ms)
    find all products
      ✓ returns an array of products (5 ms)
    updating a product
      ✓ returns an update result (8 ms)
    deleting a product
      ✓ returns a delete result (3 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        3.606 s
Ran all test suites matching /products/i.