Testing
Unit testing
makes sure that individual methods on a class are working correctly
Integration Testing
tests the full flow of a feature
Unit Testing
Dependency diagram of AuthService
In this testing we are going to test siginup()
and signin()
methods of AuthService class. As you can see in the constructor of the AUthService class, this class depends on UsersService which in tern depends on Users Repository which depends on SQLite. To just test AuthService, we have to deal with dependency nightmare.
src/users/auth.service.ts
import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { UsersService } from './users.service'; import { randomBytes, scrypt as _scrypt } from 'crypto'; import { promisify } from 'util'; const scrypt = promisify(_scrypt); @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async signup(email: string, password: string) { const users = await this.usersService.find(email); if (users.length) { throw new BadRequestException('email in use'); } // Hash the user's password // Generate a salt const salt = randomBytes(8).toString('hex'); // Hash the salt and the password together const hash = (await scrypt(password, salt, 32)) as Buffer; // Join the hashed result and the salt together const hashedPassword = `${salt}.${hash.toString('hex')}`; // Create a new user and save it const user = await this.usersService.create(email, hashedPassword); // return the user return user; } async signin(email: string, password: string) { const [user] = await this.usersService.find(email); if (!user) { throw new NotFoundException('user not found.'); } const [salt, storedHash] = user.password.split('.'); const hash = (await scrypt(password, salt, 32)) as Buffer; if (storedHash !== hash.toString('hex')) { throw new BadRequestException('Bad credentials.'); } return user; } }
In order to make our testing more straightforward, we are going to make use of Dependency Injection
. Via dependency injection, we will avoid creating all of the dependencies for testing AuthService.
We’re going to make a fake copy of UsersService which is a temporary class taht we define in our test file. We’re going to create the instance of AuthService by using FakeUsersService.
Testing Setup
create a testing script.
src/users/auth.service.spec.ts
import { Test } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { UsersService } from './users.service'; import { User } from './user.entity'; describe('AuthService', () => { let service: AuthService; beforeEach(async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); }); });
describe(name, fn)
creates a block that groups together several related tests.
beforeEach(fn, timeout)
Runs a function before each of the tests in this file runs. If the function returns a promise or is a generator, Jest waits for that promise to resolve before running the test. Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before aborting. Note: The default timeout is 5 seconds. This is often useful if you want to reset some global state that will be used by many tests.
fakeUsersService
const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), };
To avoid dealing with all dependencies that are required for using AuthService, we create fakeUserService
. This will substitute real UsersService in the testing environment.
Creating the instnace of AuthService
const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService);
The Test
class is useful for providing an application execution context that essentially mocks the full Nest runtime, but gives you hooks that make it easy to manage class instances, including mocking and overriding.
The Test class has a createTestingModule()
method that takes a module metadata object as its argument (the same object you pass to the @Module() decorator). This method returns a TestingModule
instance which in turn provides a few methods.
compile()
method bootstraps a module with its dependencies (similar to the way an application is bootstrapped in the conventional main.ts file using NestFactory.create()), and returns a module that is ready for testing.compile()
method is asynchronous and therefore has to be awaited.
Once the module is compiled you can retrieve any static instance it declares (controllers and providers) using the get()
method.
Whenever we create an instance of one of those classes, the DI container will create instances of all the dependencies of that class as well. The list of providers
is a list of classes we want to register inside the DI container.
{ provide: UsersService, useValue: fakeUsersService, }
This object can be traslated in human language as “if anyone asks for a copy of the UsersService, then give them the value fakeUsersService”.
To create a instance of the AuthService, rather than reaching out and calling the real find() and the real create(), which required the use of the UsersRepository and SQLite, we're going to instead run these very fake implemented methods instead.
Writing Tests
Testing signup() method
🧪 Ensuring Password Gets Hashed
We’re going to test if the signup() method hashes the user’s passowrd as expected.
signup() of AuthService
... import { randomBytes, scrypt as _scrypt } from 'crypto'; import { promisify } from 'util'; const scrypt = promisify(_scrypt); @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async signup(email: string, password: string) { const users = await this.usersService.find(email); if (users.length) { throw new BadRequestException('email in use'); } // Hash the user's password // Generate a salt const salt = randomBytes(8).toString('hex'); // Hash the salt and the password together const hash = (await scrypt(password, salt, 32)) as Buffer; // Join the hashed result and the salt together const hashedPassword = `${salt}.${hash.toString('hex')}`; // Create a new user and save it const user = await this.usersService.create(email, hashedPassword); // return the user return user; } ... }
auth.service.spec.ts
import { Test } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { UsersService } from './users.service'; import { User } from './user.entity'; describe('AuthService', () => { let service: AuthService; beforeEach(async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); }); it('creates a new user with a salted and hashed password', async () => { const user = await service.signup('[email protected]', 'secret'); expect(user.password).not.toEqual('secret'); const [salt, hash] = user.password.split('.'); expect(salt).toBeDefined(); expect(hash).toBeDefined(); }); });
According to the signup()
method of AuthService, it should return the salted and hashed password instaed of plain text password the user entered. To test this functionality, we wrote the testing code as follows:
it('creates a new user with a salted and hashed password', async () => { const user = await service.signup('[email protected]', 'secret'); expect(user.password).not.toEqual('secret'); const [salt, hash] = user.password.split('.'); expect(salt).toBeDefined(); expect(hash).toBeDefined(); });
Test Result
npm run test:watch
The test was passed.
🧪 Preventing users from using an existing email
In this test, we’re going to check if the singup() method throws an error if tuser signs up with an email that is already in use.
The siginup() method first uses find() method of UsersService to check if there are an user whose email is the entered email. So, the find() method of the fakeUsersService in our teting code should return a user taht has the etered email.
Here the conflict comes. In the first test we wrote (checking if the signup() method hashed the password),fakeUsersService.find()
should return Promise.resolve([])
to complete the user creating logic. However, in the second test we’re writing, the fakeUsersService.find() should return an existing user. The two tests have different requirements.
To solve this conflict, we’re going to modify the testing setup code so that we can redefine fakeUsersService’s methods according to our test requirements.
modified testing setup code
import { Test } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { UsersService } from './users.service'; import { User } from './user.entity'; describe('AuthService', () => { let service: AuthService; let fakeUsersService: Partial<UsersService>; beforeEach(async () => { // Create a fake copy of the users service fakeUsersService = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); }); ... });
So, now let’s continue writing our second test code.
First, we redefined the fakeUsersService.find()
method to return a user.
Then, We have to check if the signup() method throws a BadRequestException
if the user with the entered email exists.
it('throws an error if user signs up with email that is in use', async () => { fakeUsersService.find = () => Promise.resolve([ { id: 1, email: '[email protected]', password: 'asdf' } as User, ]); await expect(service.signup('test@test', 'secret')).rejects.toBeInstanceOf( BadRequestException, ); });
Testing signin() method
🧪 Chekcing if the entered email is being used
The first thing we’re going to test is that the method throws a NotFoundException
if the user treis to sing in with an unregisterd email.
it('throws if signin is called with an unused email', async () => { await expect( service.signin('[email protected]', 'secret'), ).rejects.toBeInstanceOf(NotFoundException); });
🧪 Checking Password Comparison
In this test, we’ll verify if the siginin() method correcly filters out the invalid password. For the test, we need a stored user whose password contains salt and hash so that the test can compare the entered password with the stored password. To this end, we are going to modify fakeUsersService to meet our test requirements. third-test-result.png
More Intelligent Mocks
const users: User[] = []; fakeUsersService = { find: (email: string) => { const filteredUsers = users.filter((user) => user.email === email); return Promise.resolve(filteredUsers); }, create: (email: string, password: string) => { const user = { id: Math.floor(Math.random() * 99999), email, password, } as User; users.push(user) return Promise.resolve(user); }, };
auth.service.ts
async signup(email: string, password: string) { const users = await this.usersService.find(email); if (users.length) { throw new BadRequestException('email in use'); } // Hash the user's password // Generate a salt const salt = randomBytes(8).toString('hex'); // Hash the salt and the password together const hash = (await scrypt(password, salt, 32)) as Buffer; // Join the hashed result and the salt together const hashedPassword = `${salt}.${hash.toString('hex')}`; // Create a new user and save it const user = await this.usersService.create(email, hashedPassword); // return the user return user; }
First, if we call service.signup()
, the signup() method will salt and hash the password and then call fakeUsersService’s create()
. This will store a new use with hased password in the users: User[]
array. So that the test can use this for testing passwor comparison and others.
Now it’s time to test.
it('returns a user if correct password is provided', async () => { await service.signup('[email protected]', 'secret'); const user = await service.signin('[email protected]', 'secret'); expect(user).toBeDefined(); });
Refactoring to Use Intelligent Mocks
Our previous tests can be refactored to use our new version of fakeUsersService.
🛠️ Preventing users from using an existing email
Now, we can create a user and store it to use in the tests. For this test, first we signup() a user and then call signup() agian with the same email we used for the first time. The signup()
should first check if the entered email is already in use and if it finds a user with the entered email, it should throw.
it('throws an error if user signs up with email that is in use', async () => { await service.signup('[email protected]', 'secret'); await expect( service.signup('[email protected]', 'test'), ).rejects.toBeInstanceOf(BadRequestException); });
First, we sign up a user with ‘[email protected]’. And then we check if the signup() method throws BadRequestException if the test tries to sign up with the existing email ‘[email protected]’.
Unit Testing a Controller
src/users/users.controller.ts
import { Body, Controller, Delete, Get, NotFoundException, Param, Patch, Post, Query, Session, UseGuards, } from '@nestjs/common'; import { UpdateUserDto } from './dtos/update-user.dto'; import { CreateUserDto } from './dtos/create-user.dto'; import { UsersService } from './users.service'; import { AuthService } from './auth.service'; import { CurrentUser } from './decorators/current-user.decorators'; import { UserDto } from './dtos/user.dto'; import { Serialize } from '../interceptors/serialize.interceptor'; import { User } from './user.entity'; import { AuthGuard } from '../gurads/auth.guard'; @Serialize(UserDto) @Controller('auth') export class UsersController { constructor( private usersService: UsersService, private authService: AuthService, ) {} @Get('/whoami') @UseGuards(AuthGuard) whoAmI(@CurrentUser() user: User) { return user; } @Post('/signup') async createUser(@Body() body: CreateUserDto, @Session() session: any) { // this.usersService.create(body.email, body.password); const user = await this.authService.signup(body.email, body.password); session.userId = user.id; return user; } @Post('/signin') async signin(@Body() body: CreateUserDto, @Session() session: any) { const user = await this.authService.signin(body.email, body.password); session.userId = user.id; return user; } @Post('/signout') signOut(@Session() session: any) { session.userId = null; } @Get('/:id') async findUser(@Param('id') id: string) { console.log('handler is running'); const user = await this.usersService.findOne(+id); if (!user) { throw new NotFoundException('user not found'); } return user; } @Get() findAllUsers(@Query('email') email: string) { return this.usersService.find(email); } @Delete('/:id') removeUser(@Param('id') id: string) { return this.usersService.remove(+id); } @Patch('/:id') updateUser(@Param('id') id: string, @Body() body: UpdateUserDto) { return this.usersService.updaet(+id, body); } }
Users Controller has two dependencies as you can see in the code:
constructor( private usersService: UsersService, private authService: AuthService, ) {}
First, let’s write Mock Implementations for the users.controller.
describe('UsersController', () => { let controller: UsersController; let fakeUsersService: Partial<UsersService>; let fakeAuthService: Partial<AuthService>; beforeEach(async () => { fakeUsersService = { findOne: (id: number) => { return Promise.resolve({ id, email: '[email protected]', password: 'secret', } as User); }, find: (email: string) => { return Promise.resolve([{ id: 1, email, password: 'test' }] as User[]); }, // remove: (id: number) => {}, // update: () => {} }; fakeAuthService = { // signup: () => {}, signin: (email: string, password: string) => { return Promise.resolve({ id: 1, email, password, } as User); }, }; ...
And then, we have to register our mock implementations into DI container so that the instance of UsersController can be created without dependency problem.
describe('UsersController', () => { ... const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: fakeUsersService, }, { provide: AuthService, useValue: fakeAuthService }, ], }).compile(); controller = module.get<UsersController>(UsersController); });
Fianlly, we write testing code to test methods in the Users Controller.
it('findAllUsers returns a list of users with the given email', async () => { const users = await controller.findAllUsers('[email protected]'); expect(users.length).toEqual(1); expect(users[0].email).toEqual('[email protected]'); }); it('findUser returns a single user with the given id', async () => { const user = await controller.findUser('1'); expect(user).toBeDefined(); }); it('findUser throws an error if use with given id is not found', async () => { fakeUsersService.findOne = () => null; await expect(controller.findUser('2')).rejects.toBeInstanceOf( NotFoundException, ); }); it('signin updates session object and returns user', async () => { const session = { userId: -1 }; const user = await controller.signin( { email: '[email protected]', password: 'test', }, session, ); expect(user.id).toEqual(1); expect(session.userId).toEqual(1); });
Test results
Entire code for testing users controller
users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { AuthService } from './auth.service'; import { User } from './user.entity'; import { NotFoundException } from '@nestjs/common'; describe('UsersController', () => { let controller: UsersController; let fakeUsersService: Partial<UsersService>; let fakeAuthService: Partial<AuthService>; beforeEach(async () => { fakeUsersService = { findOne: (id: number) => { return Promise.resolve({ id, email: '[email protected]', password: 'secret', } as User); }, find: (email: string) => { return Promise.resolve([{ id: 1, email, password: 'test' }] as User[]); }, // remove: (id: number) => {}, // update: () => {} }; fakeAuthService = { // signup: () => {}, signin: (email: string, password: string) => { return Promise.resolve({ id: 1, email, password, } as User); }, }; const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: fakeUsersService, }, { provide: AuthService, useValue: fakeAuthService }, ], }).compile(); controller = module.get<UsersController>(UsersController); }); it('should be defined', () => { expect(controller).toBeDefined(); }); it('findAllUsers returns a list of users with the given email', async () => { const users = await controller.findAllUsers('[email protected]'); expect(users.length).toEqual(1); expect(users[0].email).toEqual('[email protected]'); }); it('findUser returns a single user with the given id', async () => { const user = await controller.findUser('1'); expect(user).toBeDefined(); }); it('findUser throws an error if use with given id is not found', async () => { fakeUsersService.findOne = () => null; await expect(controller.findUser('2')).rejects.toBeInstanceOf( NotFoundException, ); }); it('signin updates session object and returns user', async () => { const session = { userId: -1 }; const user = await controller.signin( { email: '[email protected]', password: 'test', }, session, ); expect(user.id).toEqual(1); expect(session.userId).toEqual(1); }); });
End to End Test
With an end to end test, we are attempting to make sure that a lot of different pieces of our application are working together and working as expected. So rather than trying to call one single method and make sure that that method does the right thing, we're going to instead create an entire copy or an entire instance of our application.
We're then going to assign that instance to listen to traffic on some random port on your computer. Then inside of our test, we're going to make requests to that very temporary server that is listening to traffic.
Let's try putting together our own custom end to end test.
we're going to test out our authentication system and make sure that we can successfully sign up, sign out and sign back in to get started inside my test directory.
request.http
### Create a new user POST http://localhost:3000/auth/signup content-type: application/json { "email": "[email protected]", "password": "secret" }
resposne
HTTP/1.1 201 Created X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 33 ETag: W/"21-/doch6oHEdV/c3WU6PWTIXswJRU" Set-Cookie: session=eyJjb2xvciI6Im9yYW5nZSIsInVzZXJJZCI6MX0=; path=/; httponly,session.sig=OtPWGuimzDLePxh8BdUf1Vw2CJo; path=/; httponly Date: Tue, 05 Jul 2022 08:40:16 GMT Connection: close { "id": 1, "email": "[email protected]" }
We are goin to test if we send POST
request to /auth/signup
handler, the server returns response including id
and email
and status code 201
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('Authentication System', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('handles a sinup request', () => { const email = '[email protected]'; return request(app.getHttpServer()) .post('/auth/signup') .send({ email, password: 'secret' }) .expect(201) .then((res) => { const { id, email } = res.body; expect(id).toBeDefined(); expect(email).toEqual(email); }); }); });
If we run the test, we can see the error message Cannot set properties of unidefined (setting ‘userId’)
There's a little piece missing when we are running our app in this testing environment that is causing this error to be thrown. In development environment, we've got users module, the reports module. Both of them get imported into the app module. We then import the app module into the main.ts file and we have a bootstrap function inside there.
The bootstrap function is going to create a new nest application out of the app module and then we manually wire up a cookie session and the validation pipe and then after that we start listening for traffic on PORT 3000.
However, during testing, we completely skip over the main.ts
file. No code inside the main.ts
file is executed in any way. Instead, the end to end test is importing the app module directly and then creating an app out of the app module. We can see that very easily back inside of our beforeEach
statement.
So during testing, we completely skip over the main.ts
file and the configuration inside there that sets up cookie-session
and the validation pipe
. So at present, during testing, we have no concept of sessions and we have no concept of validating incoming requests through the validation pipe just because that stuff does not get set up. This explains the error message we are seeing at our terminal.
This error is being thrown whenever we try to set the userId property on the user's session object because the cookie-session.
main.ts
... async function bootstrap() { const app = await NestFactory.create(AppModule); app.use( cookieSession({ keys: ['cookie-secret'], // used to encrypt the information that is stored inside cookie }), ); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
To handle this issue, we're going to set cookie-session
and validation pipe
globally from within the app module
itself.
So when the app module is executed, when we create an application out of it, the app module is going to automatically set up the validation pipe
and the cookie-session
middleware.
Applying a globally scoped pipe
app.module.ts
import { Module, ValidationPipe } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; import { ReportsModule } from './reports/reports.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './users/user.entity'; import { Report } from './reports/report.entity'; @Module({ imports: [ UsersModule, ReportsModule, TypeOrmModule.forRoot({ type: 'sqlite', database: 'db.sqlite', entities: [User, Report], synchronize: true, }), ], controllers: [AppController], providers: [ AppService, // setting up a global pipe { provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true, }), }, ], }) export class AppModule {}
Applying a globally scoped middleware
The next step is to take our cookie-session
middleware, and we want to apply this as a global middleware as well. We want to make sure that it gets applied to every single request that comes into our application.
import { MiddlewareConsumer, Module, ValidationPipe } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; import { ReportsModule } from './reports/reports.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './users/user.entity'; import { Report } from './reports/report.entity'; const cookieSession = require('cookie-session'); @Module({ imports: [ UsersModule, ReportsModule, TypeOrmModule.forRoot({ type: 'sqlite', database: 'db.sqlite', entities: [User, Report], synchronize: true, }), ], controllers: [AppController], providers: [ AppService, // setting up a global pipe { provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true, }), }, ], }) export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply( cookieSession({ keys: ['cookie-secret'], // used to encrypt the information that is stored inside cookie }), ) .forRoutes('*'); } }
This configuration function is going to be called automatically whenever our application starts listening for incoming traffic.
After modifying our code, we can see taht the test passes successfully!
Creating separate Test and Dev Databases
Installing dependency
npm i @nestjs/config
Set the project to use environment file based on the NODE_ENV
We’re going to use ConfigModule
to specify which environmnet file we want to read. The ConfigService
is going to expose the informaion inside the environment file to the rest of our application.
App.module.ts
... import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV}`, }), UsersModule, ReportsModule, ], ... }) export class AppModule { ... }
isGlobal: true
We intend to use this config module throughout the rest of our application (globally). This setting means that we do not have to reimport the config module all over the place into other modules inside of a project whenever we want to get some config information.
Now, we are going to use information read by ConfigService to set TypeOrmModule. To this end, we are going to use Dependency Injection via inject
anad useFactory
options.
... @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV}`, }), UsersModule, ReportsModule, TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => { return { type: 'sqlite', database: config.get<string>('DB_NAME'), synchronize: true, entities: [User, Report], }; }, }), // TypeOrmModule.forRoot({ // type: 'sqlite', // database: 'db.sqlite', // entities: [User, Report], // synchronize: true, // }), ], ... }) export class AppModule { ... }
Specifying the runtime environment
In the ConfigModule
setting, we specified that envFilepath
should be .env.${process.env.NODE_ENV
. So, we need to make sure that whenever we run our project, this environment variable is set so that we know exactly which environment files we are going to use.
Installing dependency
npm i cross-env
cross-env
helps you to set environment variables corss-platoform.
Setting NODE_ENV in package.json scripts
package.json
{ ... "scripts": { "prebuild": "rimraf dist", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "cross-env NODE_ENV=development nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "cross-env NODE_ENV=test jest", "test:watch": "cross-env NODE_ENV=test jest --watch --maxWorkers=1", "test:cov": "cross-env NODE_ENV=test jest --coverage", "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json --maxWorkers=1" }, ... }
Making sure that the git will ignore env file
.gitignore
... .env*
Problem: Error being thrown if we run the test more than twice.
You might have noticed that if we run the test more than twice, the test will throw an error. This is because the first test create an user and in the second test, it tries to create a user that has the same email as the user created in the first test.
To solve this issue, we have to make the test delete test database for each test.
We can use globalBeforeEach
for this.
test/jest-e2e.json
{ ... "setupFilesAfterEnv": ["<rootDir>/setup.ts"] }
test/setup.ts
import { rm } from 'fs/promises'; import { join } from 'path'; global.beforeEach(async () => { try { await rm(join(__dirname, '..', 'test.sqlite')); } catch (err) {} });
Writing additional testing
As we finished setting the testing environment, we are finally going to write a new e2e testing code for authentication. The following codes tests if we siginup a new user, we get status code 201 and by using /auth/whoami
route, we’ll check if we get the correct user that are equal to the user we just signed up.
it('signup as a new user and then get the currently logged in user', async () => { const email = '[email protected]'; const res = request(app.getHttpServer()) .post('/auth/signup') .send({ email, password: 'secret' }) .expect(201); const cookie = res.get('Set-Cookie'); const { body } = await request(app.getHttpServer()) .get('/auth/whoami') .set('Cookie', cookie) .expect(200); expect(body.email).toEqual(email); });
The entire end to end teseting code is as follows:
auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('Authentication System', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('handles a sinup request', () => { const email = '[email protected]'; return request(app.getHttpServer()) .post('/auth/signup') .send({ email, password: 'secret' }) .expect(201) .then((res) => { const { id, email } = res.body; expect(id).toBeDefined(); expect(email).toEqual(email); }); }); it('signup as a new user and then get the currently logged in user', async () => { const email = '[email protected]'; const res = request(app.getHttpServer()) .post('/auth/signup') .send({ email, password: 'secret' }) .expect(201); const cookie = res.get('Set-Cookie'); const { body } = await request(app.getHttpServer()) .get('/auth/whoami') .set('Cookie', cookie) .expect(200); expect(body.email).toEqual(email); }); });