Nest JS Writing Test cases for controller and services Part-7
Nest JS Writing Test cases for controller and services Part-7
Blog | Name | Link |
Part-1 | Nest JS Building REST APIs using Mongo DB Part-1 | devs.tkssharma.com/blog/nestjs-building-res.. |
Part-2 | Nest JS APIs with mongoose Mongo DB Database Part-2 | devs.tkssharma.com/blog/how-to-build-nestjs.. |
Part-2 | Nest JS APIs with mongoose Mongo DB Database Part-2 | devs.tkssharma.com/blog/how-to-build-nestjs.. |
Part-3 | Nest JS Building Auth Service with JWT Tokens Part-3 | devs.tkssharma.com/blog/how-to-build-nestjs.. |
Part-4 | Nest JS with Mongo DB Managing Relationships Part-4 | devs.tkssharma.com/blog/how-to-build-nestjs.. |
Part-5 | How to manage environment variables in Nest JS Part-5 | devs.tkssharma.com/blog/how-to-manage-envir.. |
Part-6 | User On boarding | devs.tkssharma.com/blog/how-to-build-nestjs.. |
Github Link github.com/tkssharma/blogs/tree/master/nest..
In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's viewpoint and will also do testing
Github Link github.com/tkssharma/blogs/tree/master/nest..
What is NestJS?
As described in the Nestjs website, Nestjs is a progressive Node.js framework for building efficient, reliable and scalable server-side applications.
Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS communities.
- A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp. Angular .
- Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, you are free to choose Expressjs or Fastify as the background engine.
- Nestjs provides a lot of third party project integrations, from database operations, such as Mongoose, TypeORM, etc. to Message Brokers, such as Redis, RabbitMQ, etc.
If you are new to Nestjs like me but has some experience of Angular , TypeDI or Spring WebMVC, bootstraping a Nestjs project is really a piece of cake.
Make sure you have installed the latest Nodejs.
npm i -g @nestjs/cli
When it is finished, there is a nest
command available in the Path
. The usage of nest
is similar with ng
(Angular CLI), type nest --help
in the terminal to list help for all commands.
❯ nest --help
Usage: nest <command> [options]
Options:
-v, --version Output the current version.
-h, --help Output usage information.
Commands:
new|n [options] [name] Generate Nest application.
build [options] [app] Build Nest application.
start [options] [app] Run Nest application.
info|i Display Nest project details.
update|u [options] Update Nest dependencies.
add [options] <library> Adds support for an external library to your project.
generate|g [options] <schematic> [name] [path] Generate a Nest element.
Available schematics:
┌───────────────┬─────────────┐
│ name │ alias │
│ application │ application │
│ class │ cl │
│ configuration │ config │
│ controller │ co │
│ decorator │ d │
│ filter │ f │
│ gateway │ ga │
│ guard │ gu │
│ interceptor │ in │
│ interface │ interface │
│ middleware │ mi │
│ module │ mo │
│ pipe │ pi │
│ provider │ pr │
│ resolver │ r │
│ service │ s │
│ library │ lib │
│ sub-app │ app │
└───────────────┴─────────────┘
Now generate a Nestjs project via:
nest new nestjs-sample
Open it in your favorite IDEs, such as Intellij WebStorm or VSCode.
Exploring the project files
Expand the project root, you will see the following like tree nodes.
.
├── LICENSE
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
The default structure of this project is very similar with the one generated by Angular CLI.
- src/main.ts is the entry file of this application.
- *src/app** is the top level component in a nest application.
- There is an app.module.ts is a Nestjs
Module
which is similar with AngularNgModule
, and used to organize codes in the logic view. - The app.service.ts is an
@Injectable
component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with@Injectable
. - The app.controller.ts is the controller of MVC, to handle incoming request, and responds the handled result back to client. The annotatoin
@Controller()
is similar with Spring MVC's@Controller
. - The app.controller.spec.ts is test file for app.controller.ts. Nestjs uses Jest as testing framework.
- There is an app.module.ts is a Nestjs
- test folder is for storing e2e test files.
Testing Nestjs applications
In the previous posts, I have write a lot of testing codes to verify if our application is working as expected.
Nestjs provides integration with with Jest and Supertest out-of-the-box, and testing harness for unit testing and end-to-end (e2e) test.
Nestjs test harness
Like the Angular 's TestBed
, Nestjs provide a similar Test
facilities to assemble the Nestjs components for your testing codes.
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...
],
}).compile();
service = module.get<UserService>(UserService);
});
Similar to the attributes in the @Module
decorator, creatTestingModule
defines the components that will be used in the tests.
We have demonstrated the methods to test a service in Nestjs applications, eg. in the post.service.spec.ts
.
To isolate the dependencies in a service, there are several approaches.
Create a fake service to replace the real service, assemble it in the
providers
.providers: [ { provide: UserService, useClass: FakeUserService } ],
Use a mock instance instead.
providers: [ provide: UserService, useValue: { send: jest.fn() } ],
For simple service providers, you can escape from the Nestjs harness, and create a simple fake dependent service, and use
new
to instantize your service in thesetup
hooks.
You can also import a module in Test.createTestingModule
.
Test.createTestingModule({
imports: []
})
To replace some service in the imported modules, you can override
it.
Test.createTestingModule({
imports: []
})
.override(...)
Jest Tips and Tricks
Nestjs testing is heavily dependent on Jest framework. I have spent a lot of time to research testing all components in Nestjs applications.
Mocking external classes or functions
For example the mongoose.connect
will require a real mongo server to connect, to mock the createConnection
of mongoose
.
Set up mocks before importing it.
jest.mock('mongoose', () => ({
createConnection: jest.fn().mockImplementation(
(uri:any, options:any)=>({} as any)
),
Connection: jest.fn()
}))
//...
import { Connection, createConnection } from 'mongoose';
//
When a database provider is instantized, assert the createConnection
is called.
it('connect is called', () => {
//expect(conn).toBeDefined();
//expect(createConnection).toHaveBeenCalledTimes(1); // it is 2 here. why?
expect(createConnection).toHaveBeenCalledWith("mongodb://localhost/blog", {
useNewUrlParser: true,
useUnifiedTopology: true,
//see: https://mongoosejs.com/docs/deprecations.html#findandmodify
useFindAndModify: false
});
})
Mock parent classes through prototype
Have a look at the local auth guard tests.
Mock the method canActivate
in the parent prototype.
describe('LocalAuthGuard', () => {
let guard: LocalAuthGuard;
beforeEach(() => {
guard = new LocalAuthGuard();
});
it('should be defined', () => {
expect(guard).toBeDefined();
});
it('should return true for `canActivate`', async () => {
AuthGuard('local').prototype.canActivate = jest.fn(() =>
Promise.resolve(true),
);
AuthGuard('local').prototype.logIn = jest.fn(() => Promise.resolve());
expect(await guard.canActivate({} as ExecutionContext)).toBe(true);
});
});
Extract the functionality into functions as possible
Let's have a look at the user.model.ts
. Extract the pre save
hook method and custom comparePassword
method into standalone functions.
async function preSaveHook(next) {
// Only run this function if password was modified
if (!this.isModified('password')) return next();
// Hash the password
const password = await hash(this.password, 12);
this.set('password', password);
next();
}
UserSchema.pre<User>('save', preSaveHook);
function comparePasswordMethod(password: string): Observable<boolean> {
return from(compare(password, this.password));
}
UserSchema.methods.comparePassword = comparePasswordMethod;
It is easy to test them like simple functions.
describe('preSaveHook', () => {
test('should execute next middleware when password is not modified', async () => {
const nextMock = jest.fn();
const contextMock = {
isModified: jest.fn()
};
contextMock.isModified.mockReturnValueOnce(false);
await preSaveHook.call(contextMock, nextMock);
expect(contextMock.isModified).toBeCalledWith('password');
expect(nextMock).toBeCalledTimes(1);
});
test('should set password when password is modified', async () => {
const nextMock = jest.fn();
const contextMock = {
isModified: jest.fn(),
set: jest.fn(),
password: '123456'
};
contextMock.isModified.mockReturnValueOnce(true);
await preSaveHook.call(contextMock, nextMock);
expect(contextMock.isModified).toBeCalledWith('password');
expect(nextMock).toBeCalledTimes(1);
expect(contextMock.set).toBeCalledTimes(1);
});
});
Simple Test example
import { Test, TestingModule } from '@nestjs/testing';
import { AppService } from './app.service';
import { AppController } from './app.controller';
describe('AppController', () => {
let appController: AppController;
let service: AppService;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [
{
provide: AppService,
useValue: {
constructor: jest.fn(),
getHello: jest.fn()
}
}
],
}).compile();
service = app.get<AppService>(AppService);
appController = app.get<AppController>(AppController);
});
it('should be defined', () => {
expect(appController).toBeDefined();
});
it('getHello',async () => {
jest.spyOn(service, "getHello").mockReturnValue("Hello");
expect(appController.getHello()).toEqual("Hello");
})
});
Testing a service
import { Test, TestingModule } from '@nestjs/testing';
import { AppService } from './app.service';
import { LoggerService } from './logger/logger.service';
describe('AppService', () => {
let logger: LoggerService;
let service: AppService;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
providers: [
AppService,
{
provide: 'LoggerServiceAppService',
useValue: {
constructor: jest.fn(),
log: jest.fn()
}
}
],
})
.compile();
service = app.get<AppService>(AppService);
logger = app.get<LoggerService>('LoggerServiceAppService');
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('getHello', async () => {
jest.spyOn(logger, "log").mockImplementation((message: string) => {
console.log(message);
})
const result = service.getHello();
expect(result).toEqual('Hello World!');
expect(logger.log).toBeCalledWith("Hello World");
})
});
User Controller spec
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { lastValueFrom, of } from 'rxjs';
describe('UserController', () => {
let controller: UserController;
let service: UserService;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
providers: [
{
provide: UserService,
useValue: {
findById: jest.fn(),
},
},
],
controllers: [UserController],
}).compile();
controller = app.get<UserController>(UserController);
service = app.get<UserService>(UserService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('getUser', async () => {
jest
.spyOn(service, 'findById')
.mockImplementationOnce((id: string, withPosts: boolean) =>
of({
username: 'hantsy',
password: 'mysecret',
email: 'hantsy@example.com',
firstName: 'hantsy',
lastName: 'bai',
} as any),
);
const user = await lastValueFrom(controller.getUser('id', false));
expect(user.firstName).toBe('hantsy');
expect(user.lastName).toBe('bai');
expect(service.findById).toBeCalledWith('id', false);
});
});
User service test spec
import { Test, TestingModule } from '@nestjs/testing';
import { Model, FilterQuery } from 'mongoose';
import { lastValueFrom, of } from 'rxjs';
import { USER_MODEL } from '../database/database.constants';
import { User } from '../database/user.model';
import { SendgridService } from '../sendgrid/sendgrid.service';
import { RoleType } from '../shared/enum/role-type.enum';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let model: Model<User>;
let sendgrid: SendgridService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: USER_MODEL,
useValue: {
findOne: jest.fn(),
exists: jest.fn(),
create: jest.fn(),
},
},
{
provide: SendgridService,
useValue: {
send: jest.fn(),
},
},
],
}).compile();
service = module.get<UserService>(UserService);
sendgrid = module.get<SendgridService>(SendgridService);
model = module.get<Model<User>>(USER_MODEL);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('save ', async () => {
const sampleData = {
username: 'hantsy',
email: 'hantsy@example.com',
firstName: 'hantsy',
lastName: 'bai',
password: 'mysecret',
};
const msg = {
from: 'service@example.com', // Use the email address or domain you verified above
subject: 'Welcome to Nestjs Sample',
templateId: 'welcome',
personalizations: [
{
to: 'hantsy@example.com',
dynamicTemplateData: { name: 'hantsy bai' },
},
],
};
const saveSpy = jest.spyOn(model, 'create').mockImplementation(() =>
Promise.resolve({
_id: '123',
...sampleData,
} as any),
);
const pipeMock = {
pipe: jest.fn(),
};
const pipeSpy = jest.spyOn(pipeMock, 'pipe');
const sendSpy = jest
.spyOn(sendgrid, 'send')
.mockImplementation((data: any) => {
return of(pipeMock);
});
const result = await lastValueFrom(service.register(sampleData));
expect(saveSpy).toBeCalledWith({ ...sampleData, roles: [RoleType.USER] });
expect(result._id).toBeDefined();
//expect(sendSpy).toBeCalledWith(msg);
//expect(pipeSpy).toBeCalled();
});
it('findByUsername should return user', async () => {
jest.spyOn(model, 'findOne').mockImplementation(
(filter?: FilterQuery<User>) =>
({
exec: jest.fn().mockResolvedValue({
username: 'hantsy',
email: 'hantsy@example.com',
} as User),
} as any),
);
const foundUser = await lastValueFrom(service.findByUsername('hantsy'));
expect(foundUser).toEqual({
username: 'hantsy',
email: 'hantsy@example.com',
});
expect(model.findOne).lastCalledWith({ username: 'hantsy' });
expect(model.findOne).toBeCalledTimes(1);
});
describe('findById', () => {
it('return one result', async () => {
jest.spyOn(model, 'findOne')
.mockImplementation(
(filter?: FilterQuery<User>) =>
({
exec: jest.fn().mockResolvedValue({
username: 'hantsy',
email: 'hantsy@example.com',
} as User),
} as any),
);
const foundUser = await lastValueFrom(service.findById('hantsy'));
expect(foundUser).toEqual({
username: 'hantsy',
email: 'hantsy@example.com',
});
expect(model.findOne).lastCalledWith({ _id: 'hantsy' });
expect(model.findOne).toBeCalledTimes(1);
});
it('return a null result', async () => {
jest
.spyOn(model, 'findOne')
.mockImplementation((filter?: FilterQuery<User>) => ({
exec: jest.fn().mockResolvedValue(null) as any,
} as any));
try {
const foundUser = await lastValueFrom(service.findById('hantsy'));
} catch (e) {
expect(e).toBeDefined();
}
});
it('parameter withPosts=true', async () => {
jest
.spyOn(model, 'findOne')
.mockImplementation((filter?: FilterQuery<User>) => ({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValue({
username: 'hantsy',
email: 'hantsy@example.com',
} as User),
} as any));
const foundUser = await lastValueFrom(service.findById('hantsy', true));
expect(foundUser).toEqual({
username: 'hantsy',
email: 'hantsy@example.com',
});
expect(model.findOne).lastCalledWith({ _id: 'hantsy' });
expect(model.findOne).toBeCalledTimes(1);
});
});
describe('existsByUsername', () => {
it('should return true if exists ', async () => {
const existsSpy = jest
.spyOn(model, 'exists')
.mockImplementation((filter: any) => {
return {
exec: jest.fn().mockResolvedValue({
_id: 'test',
} as any),
} as any;
});
const result = await lastValueFrom(service.existsByUsername('hantsy'));
expect(existsSpy).toBeCalledWith({ username: 'hantsy' });
expect(existsSpy).toBeCalledTimes(1);
expect(result).toBeTruthy();
});
it('should return false if not exists ', async () => {
const existsSpy = jest
.spyOn(model, 'exists')
.mockImplementation((filter: any) => {
return {
exec: jest.fn().mockResolvedValue(null),
} as any;
});
const result = await lastValueFrom(service.existsByUsername('hantsy'));
expect(existsSpy).toBeCalledWith({ username: 'hantsy' });
expect(existsSpy).toBeCalledTimes(1);
expect(result).toBeFalsy();
});
});
describe('existsByEmail', () => {
it('should return true if exists ', async () => {
const existsSpy = jest
.spyOn(model, 'exists')
.mockImplementation((filter: any) => {
return {
exec: jest.fn().mockResolvedValue({
_id: 'test',
} as any),
} as any;
});
const result = await lastValueFrom(
service.existsByEmail('hantsy@example.com'),
);
expect(existsSpy).toBeCalledWith({ email: 'hantsy@example.com' });
expect(existsSpy).toBeCalledTimes(1);
expect(result).toBeTruthy();
});
it('should return false if not exists ', async () => {
const existsSpy = jest
.spyOn(model, 'exists')
.mockImplementation((filter: any) => {
return {
exec: jest.fn().mockResolvedValue(null),
} as any;
});
const result = await lastValueFrom(
service.existsByEmail('hantsy@example.com'),
);
expect(existsSpy).toBeCalledWith({ email: 'hantsy@example.com' });
expect(existsSpy).toBeCalledTimes(1);
expect(result).toBeFalsy();
});
});
});
End-to-end testing
Nestjs integrates supertest to send a request to the server side.
Use beforeAll
and afterAll
to start and stop the application, use request
to send a http request to the server and assert the response result.
import * as request from 'supertest';
//...
describe('API endpoints testing (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.enableShutdownHooks();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
// an example of using supertest reqruest.
it('/posts (GET)', async () => {
const res = await request(app.getHttpServer()).get('/posts').send();
expect(res.status).toBe(200);
expect(res.body.length).toEqual(3);
});
}
More details for the complete e2e tests Github Link github.com/tkssharma/blogs/tree/master/nest..