Developing a NestJS app is a joyful experience. Let's see how we can add unit tests and end-to-end tests to a NestJS application. I have learned a lot from this article written by Débora Barreto Ornellas and this write-up is based mostly on it.
There are five high-level steps that we need to do.
- Create a sample NestJS application
- Add unit test cases
- Run unit test cases
- Add end to end test cases
- Run all tests
Detailed steps below:
1. Creating a sample NestJS application
Let's start by creating a NestJS application called ‘StudentGPA’. Go to your folder and run the below command in the VSCode terminal.
nest new StudentGPA
Create two services by executing the below commands:
nest g service student
nest g service api
Also, do an npm install and make sure the project runs as expected:
npm install
nest start
After confirming that the app is running,
your file explorer will look like this:
The concept of this app is simple. The class StudentService defined in student.service.ts defines a function that accepts the first name and second name of the student and returns the GPA. The StudentService class uses the other class called ‘ApiService’ defined in api.service.ts to get the student information. In real scenarios, this class will be communicating with a database or another microservice.
The annotated files are the test files that are automatically created. Please note the extension, .spec.ts. Unit testing is made possible in NestJS by using another framework called JestJS. The Jest framework search for the files with these extensions.
As a first step let's focus only on writing unit tests. This means we will be working in app.controller.ts, student.service.ts, and api.service.ts. We also need to add the getGPA function to the controller class so that it is available for consumers.
A modified app.controller.ts (AppController class) will look like below:
Now, we need to add ‘getGpa’ in ‘studentService’. Once added, studentService (student.service.ts) will look like below:
As you can see on line# 14, we are calling ‘getStudent’ method of ‘api.service’.
Now, ApiService class will look like the below:
You may notice that in Line#8, the url variable is incorrect. The exact value does not matter because our focus is on unit testing and in the unit testing, we will never hit the real endpoints.
Please do a ‘nest start’ to make sure everything is working as expected.
2. Adding unit test cases
Before we begin, let's refresh the basic concepts of unit testing.
Unit testing focuses on writing tests for the smallest possible units. In most cases, they are functions defined in classes. MethodA in a class may be calling MethodB in another class. However, a unit test of MethodA is focused only on the logic of MethodA, not MethodB. Unit tests shouldn’t be dependent on the environment in which they are being run, and they are supposed to be fast. To write isolated unit tests, it’s common to mock all dependencies of a method/service. In the StudentService unit test, we’ll mock ApiService by creating an ApiServiceMock class.
Test Doubles: Fakes, stubs, and mocks all belong to the category of test doubles. A test double is an object or system you use in a test instead of something else.
Fakes: an object with limited capabilities (for the purposes of testing), e.g. a fake web service. Fake has business behavior. You can drive a fake to behave in different ways by giving it different data. Fakes can be used when you can’t use a real implementation in your test.
Mock: an object on which you set expectations. A mock has expectations about the way it should be called, and a test should fail if it’s not called that way. Mocks are used to test interactions between objects.
Stub: an object that provides predefined answers to method calls. A stub has no logic and only returns what you tell it to return.
In case you are interested, here is a good discussion on fake/mock/stub.
Spy: Spy, spies on the caller. Often used to make sure a particular method has been called.
For unit tests, we will focus on three classes, which are: app.controller.spec.ts(Test class of AppController), student.service.spec.ts(Test class of StudentService), and api.service.spec.ts (Test class of ApiService).
There are multiple approaches that we can take here. One of them is using Mock functions provided by the Jest framework. Mock functions allow you to test the links between code by erasing the actual implementation of a function. Another one is using mock classes.
Let's explore the mock function approach in app.controller.spec.ts.
Line#1: The ‘Test’ class from ‘@nestjs/testing’ provides a test execution context by mocking the full Nest runtime. It also provides hooks to help mock and overriding.
Line#8: A variable called ‘spyService’ is declared for spying the invocation of ‘StudentService
Line#17-20: The compile method of this object bootstraps a module with its dependencies and returns a module that is ready for testing.
Line#23: spyService is defined
Line#13 -14: A mock function for ‘getGPA’ is defined
Line#28: Verify getHello() method of AppController
Line#35: Verify getGpa of studentService has been called
Line#44: Verify the return value of getGpa is the same as the expected value
Now we will explore the Mock class approach in the case of student.service.spec.ts as provided below:
Line#1: The ‘Test’ class from ‘@nestjs/testing’ provides a test execution context by mocking the full Nest runtime. It also provides hooks to help mock and overriding.
Line#5–13: Mock class
Line#23: The ‘createTestingModule’ method of ‘Test’ class takes a module metadata object and returns a TestingModule instance.
Line#24: The mock class implementation of ‘ApiService’ is provided to be used instead of the real implementation.
Line#25: The compile method of this object bootstraps a module with its dependencies and returns a module (Line#27) that is ready for testing.
Line#31: Verifying that the studentService object is defined.
Line#38: Verify that the received GPA is the same as the expected GPA
The next one is api.service.spec.ts, which is the test class for ApiService.
It also follows the same pattern as the above-explained classes. However, there is only one basic test that checks for the existence of the service instance.
3. Run unit test cases
Now that we have three different test classes up and running, do an ‘npm test’ and make sure all of the test cases are successful. Here is the complete source code for this project in case you need it.
4. Add end to end test cases
When you created the NestProject, the framework created a folder called ‘test’ and a file ‘app.e2e-spec.ts’ automatically. We will be adding code to this file. Drag and drop the folder ‘test’ so that it is under the ‘src’ folder. By default Jest searches for test files only under the ‘src’ folder.
Also, add below two properties (“collectCoverage”: true, “verbose”: true)to the Jest configuration section of package.json file as shown below.
This will output greater details of the test in tabular format.
Now, let’s refresh a few concepts about the end to end testing.
Unlike unit testing, which focuses on individual modules and classes, end-to-end (e2e) testing covers the interaction of classes and modules, closer to the kind of interaction that end-users will have. Automated end-to-end tests help us ensure that the overall behavior of the system is correct.
In an E2E test, the idea is to also call your external services, but we will mock the HttpService for simplicity. This means we’ll call the APIService and we’ll use the Jest function spyOn to simulate our HTTP response. Nest makes it easy to use the Supertest library to simulate HTTP requests.
The overall code pattern of end to end test is similar to that of the unit testing class that we have seen before. There are some new concepts that we address soon. Below is the completed file for app.e2e-spec.ts.
Line#2: Importing ‘Supertest’. Supertest provides a high-level abstraction for testing HTTP. This helps to send requests to the server and then test the response.
Line#4: INestApplication, Interface defining the core NestApplication object.
Line#7: Importing AxiosResponse. An Axios response is a javascript object with several properties, including data
, which contains the parsed response body.
Line#8: Importing ‘of’ from Rxjs. of() is used to return a fake response. It emits its parameters as single emissions immediately on subscription and then sends the complete
notification. For more, check out this discussion.
Line#14–23: Importing dependencies, creating the test module, initializing it.
Line#26–35: Defining an expected response.
Line#36: Spying the implementation of the HTTP get method and providing it with a mock response.
Line#38: Simulate the invocation of HTTP GET call on the AppController, by specifying the routing path and query parameters. Also, an expectation is chained.
5. Run all tests
Assuming that everything so far is correct, while running the test, you should be presented with the below output.
Hope this has helped you in cracking the unit testing fundamentals of the NestJS app. Here is the complete source code for this project.