End to End Tests for our Storefront Backend
This article is Part 10 in a 10 Part Series.
- Part 1 - Bootstrapping a Nest.js Project on Ubuntu
- Part 2 - Create Nest.js Migrations
- Part 3 - Create Nest.js CRUD Resources
- Part 4 - Create Nest.js Seeders
- Part 5 - Create a Customer Module in Nest.js
- Part 6 - Nest.js Authentication
- Part 7 - Create a Products Module in Nest.js
- Part 8 - Create a Stripe Module in Nest.js
- Part 9 - Create an Orders Module in Nest.js
- Part 10 - This Article
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
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:
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:
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:
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.