Best Practices & Common Pitfalls when Testing My NestJS App

NestJSTesting
Yuval hazaz
Yuval hazaz
Nov 16, 2022
Best Practices & Common Pitfalls when Testing My NestJS AppBest Practices & Common Pitfalls when Testing My NestJS App

NestJS is a powerful Node.js framework for backend development. It has default support for TypeScript while allowing developers to code in pure JavaScript. For testing, NestJS provides built-in support for Jest while remaining agnostic to other testing tools if needed. Moreover, NestJS makes the Nest dependency injection system available in the testing environment so that developers can easily mock components in their unit testing tasks. Testing NestJS applications is somewhat similar to other testing projects. In this post, we share the key “Dos” and “Don’ts” that will help you to successfully test your next creation and will show how Amplication helps to ease the process.

Best practices for testing NestJS applications

Choose test cases to automate wisely

In testing, there’s a famous test pyramid to demonstrate how much effort we should spend on each testing type. The lower the test level, the more tests it should cover. With end-to-end tests, teams should only implement sanity tests or tests for critical features since they require much more effort to create and maintain compared to other tests such as unit or contract tests.

Pyramid Figure 1: Pyramid showing the number of tests corresponding to the test level

Design tests independently

Tests should be designed in such a way that they can be run in any order. Tests should definitely not depend on each other, or use the same test data, because this will lead to flaky test runs in the future when the order of the tests is changed or the tests are executed in parallel. Introduce Mocks for Minimizing Dependencies Mocking in unit testing is a must since a software project needs the unit test to quickly give feedback so that software engineers know which test is broken after code is updated. Moreover, even for integration tests like API testing, mocking should be considered because it detaches the use of third-party dependencies, which helps keep tests stable as well as speed up test executions.

Avoiding Common Pitfalls in NestJS Application Testing

Adopting best practices is great, but not enough to get the best return on your investment for automation testing. Teams should also be aware of the common pitfalls listed below.

Avoid automating all Manual tests

Not every test can be automated. There are some tests we have to execute manually such as testing whether mail requests are saved to the queue system when the mail service is down or exploratory testing.

Manual tests are not written for automation, meaning that if we apply automation for manual test cases, it will take a lot of time and effort to implement them, not to mention the time for maintaining the tests since they are more likely to be flaky. Hence, it’s not always wise to implement automated testing for existing manual test cases, and if we are automating test cases, the test cases should be designed and written in a way that enables automation.

Automation for end-to-end test cases should be minimized since implementing and maintaining these is more costly when compared to other level tests like unit testing, contract testing, or integration testing.

The more efficient approach would be to try to add more tests at a lower level first, such as unit and integration testing. Then, if the team has the capacity, they can write other test cases for the end-to-end test level. After multiple iterations and careful observation to make sure automation testing runs smoothly and covers all aspects of your software’s functionality, the existing manual effort for regression test cases can be removed.

Don’t treat automation testing like scripting

If developers have the mindset to treat automation tests just as automation scripts, the tests will end up containing duplicated code and be hard to maintain.

Instead, an automation testing project should be treated similarly to product code; it needs to follow software best practices and design patterns when implemented such as SOLID principles or page object models.

Don’t skip negative case scenarios

Every test has its own role in making sure software works as expected. For instance, if users type the wrong email address on the login screen and the web application shows a “System under maintenance” error message instead of “Email address does not exist,” the customer service operators won’t be able to accurately communicate what went wrong to the customer. Tests need to address negative situations, as well as positive scenarios.

Avoid creating trivial use cases for the sake of test coverage

Teams should be aware that writing tests help them to quickly identify if there are any bugs introduced after they change the code. Who doesn’t want to enjoy the summer holiday on the beach without receiving calls from the customer service team about an urgent issue in production users are facing. This is why tests should be written that align with business requirements, not just to increase test coverage.

Don’t overcomplicate the tests

Tests that are complicated are harder to implement and maintain. Having too many validations in the tests leads to unneeded modifications for the tests after new code is updated. Hence, only fields or web elements that are important and align with business rules should be validated in order to make the tests simple and sound.

Don’t rush into using a real third-party dependency in unit testing

Implementing tests with real third-party dependencies will make test execution slower. Moreover, teams might end up testing the third-party dependencies—instead of the product business features, as the dependencies might be down or running some experimental features. With unit testing, you should use test couples, not real third-party dependencies.

Stay away from writing too many mocks

Having to set up multiple mocks takes a lot of time and effort. If a software team has to deal with this, it’s time for software developers to break down their classes and methods into smaller ones. Unit testing makes more sense if the size of the methods and classes in the product code is minimized until they cannot be broken down anymore.

How to test your NestJS app

Features provided for testing

NestJS has built-in support for testing and uses Jest as a default test runner. If we want to mock third-party dependencies in unit testing, Nest provides support for dependency injection so that we can easily implement the test. Check out this documentation for how to get started writing tests for NestJS projects.

Example NestJS application to demonstrate testing

The best way to understand something is to actually do it. So let’s write tests for an example application to understand how testing works in NestJS. Building a software application from scratch is a daunting task and requires quite an effort. Out of this frustration, Amplification was born. Amplication is an open-source backend development tool that helps you build quality Node.js applications without wasting precious time on repetitive coding tasks. First, I’m going to show you how to quickly build a NestJS application for blog management with the help of Amplication.

Create the app

First, navigate to https://app.amplication.com/ and start creating your new application.

Pyramid Figure 2: Create a new application with Amplication

Choose the “Start from ” option to build a new application from scratch.

You should be able to see the “Create a new entity” page. Our application will need two entities: a “User” entity and a “Blog” entity. Amplication automatically creates a “User” entity, which is used for storing information such as username, first name, last name, and password.

Pyramid Figure 3: “User” entity is automatically created by Amplication

As to the “Blog” entity, we need to create this by ourselves; it includes information about the title and content of our blog.

Pyramid Figure 4: The “Blog” entity is created manually with “title” and “content” fields

We’re almost done setting up our blog management project in Amplication. The only thing left is handling the integration with GitHub so that code from Amplication can be committed to the Github repository.

Connect to GitHub account

Next, we need to allow Amplication to have read and write access to the GitHub project repository.

Pyramid Figure 5: Integration between Amplication and GitHub

After successfully integrating Amplication with the GitHub repository and committing the changes from Amplication to GitHub, the newly generated code will be ready.

Pyramid Figure 6: Code is synchronized with GitHub successfully

We’re now ready to clone the source code to our local machine and play with it. For details on how to run the application in the dev environment, please check out Amplication’s documentation.

Showcase functionality

Once the application is up and running in the dev environment, we can first try to get authenticated using an admin account with Postman.

Pyramid Figure 7: Authentication using admin account to get access token value

In order to create new user accounts or new blog posts, we will need the “access token” of users for authorization. Let’s copy the “accessToken” value here, then put it in the Authorization header to create a new guest account.

Pyramid Figure 8: Demonstration of how to use token for authorization with Postman

Pyramid Figure 9: Create a new guest account

Now we have a new user account named “guest.account,” which will let you create a new blog post using its authorization header.

Pyramid Figure 10: Successfully creating a new blog

So, now we’re finally able to create a new blog using the new user account.

We’ve finished implementing the blog management application. Now, to make sure the application works as expected, let’s write tests for it.

Unit testing

Unit testing helps us make sure the small unit in our product code works as expected. Actually, for unit testing, we don’t have to write the tests from scratch since Amplication already automatically created example test files for us.

Below is the unit testing file to check if the token service works as expected:

describe("Testing the TokenServiceBase", () => {
  let tokenServiceBase: TokenServiceBase;
  const jwtService = mock<JwtService>();
  beforeEach(() => {
	tokenServiceBase = new TokenServiceBase(jwtService);
	jwtService.signAsync.mockClear();
  });
  describe("Testing the BasicTokenService.createToken()", () => {
	it("should create valid token for valid username and password", async () => {
  	jwtService.signAsync.mockReturnValue(Promise.resolve(SIGN_TOKEN));
  	expect(
    	await tokenServiceBase.createToken(
      	VALID_CREDENTIALS.username,
      	VALID_CREDENTIALS.password
    	)
  	).toBe(SIGN_TOKEN);
	});
	it("should reject when username missing", () => {
  	const result = tokenServiceBase.createToken(
    	//@ts-ignore
    	null,
    	VALID_CREDENTIALS.password
  	);
  	return expect(result).rejects.toBe(INVALID_USERNAME_ERROR);
	});
	it("should reject when password missing", () => {
  	const result = tokenServiceBase.createToken(
    	VALID_CREDENTIALS.username,
    	//@ts-ignore
    	null
  	);
  	return expect(result).rejects.toBe(INVALID_PASSWORD_ERROR);
	});
  });
});

Here, we have three tests for “create a valid token if provided valid username and valid password”, “reject when username missing”, and “reject when password missing”. To remove the need to call to the actual implementation of the JwtService we use the “mock” feature provided by the jest-mock-extended library to mock the JwtService class:

let tokenServiceBase: TokenServiceBase;
  const jwtService = mock<JwtService>();
  beforeEach(() => {
	tokenServiceBase = new TokenServiceBase(jwtService);
	jwtService.signAsync.mockClear();
  });

Before running each test, you want to clear any data stored in the mock, e.g., initial implementation or mock name. To do this, “mockClear” is called; this is useful to restore the mock back to its original state. Running the test shows the result is “PASS,” as expected.

Pyramid Figure 11: Successful test run for unit testing

End-to-end testing simulation

Let’s try to implement an end-to-end test scenario for our blog management service by using NestJS-supported features for testing. Below is the code for implementing tests to show all the blog posts that the application currently has:

describe("Blog", () => {
  let app: INestApplication;

  beforeAll(async () => {
	const moduleRef = await Test.createTestingModule({
  	providers: [
    	{
      	provide: BlogService,
      	useValue: service,
    	},
  	],
  	controllers: [BlogController],
  	imports: [MorganModule.forRoot(), ACLModule],
	})
  	.overrideGuard(DefaultAuthGuard)
  	.useValue(basicAuthGuard)
  	.overrideGuard(ACGuard)
  	.useValue(acGuard)
  	.compile();

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

  test("GET /blogs", async () => {
	await request(app.getHttpServer())
  	.get("/blogs")
  	.expect(HttpStatus.OK)
  	.expect([
    	{
      	...FIND_MANY_RESULT[0],
      	createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(),
      	updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(),
    	},
  	]);
  });

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

Our blog module has BlogService as the provider, and BlogController as the controller:

@Module({
  imports: [BlogModuleBase],
  controllers: [BlogController],
  providers: [BlogService, BlogResolver],
  exports: [BlogService],
})
export class BlogModule {}
That’s why we need to create a fake module for our testing purpose, which includes a value for both providers and controllers. Moreover, we will use “overrideGuard” to pass the authentication check, as in order to get blog content, authentication is required:
const moduleRef = await Test.createTestingModule({
  	providers: [
    	{
      	provide: BlogService,
      	useValue: service,
    	},
  	],
  	controllers: [BlogController],
  	imports: [MorganModule.forRoot(), ACLModule],
	})
  	.overrideGuard(DefaultAuthGuard)
  	.useValue(basicAuthGuard)
  	.overrideGuard(ACGuard)
  	.useValue(acGuard)
  	.compile();
Running the test shows the result is “PASS.”

Pyramid Figure 12: Successful end-to-end test using mock technique

End-to-end testing with real APIs

Every testing type has its own role. It’s helpful to have end-to-end testing for our blog service with real HTTP requests to the actual running APIs and the actual running databases to verify the service functions as expected. Let’s implement end-to-end testing with real dependencies for full flow to demonstrate how real users would interact with the system:

/* eslint-disable import/no-unresolved */
import axios from 'axios';

describe("Test end-to-end for blog service", () => {
  axios.defaults.baseURL = 'http://localhost:3000/api/';
  let token: string;
  beforeEach(() => {
	axios.post('/login', {
  	username: 'guest.account',
  	password: 'DemoAmplication@'
	})
	.then((response) => {
  	token = response.data.access_token
	}, (error) => {
  	console.log(error);
	});
  });
  describe("Get blogs", () => {
	it("Get all created blogs", async () => {
  	await axios.get('/blogs', {
    	headers: {
      	Authorization: 'Bearer ' + token
    	}
  	}).then((response) => {
    	expect(response.status).toEqual(200)
    	expect(response.data.length).toBeGreaterThanOrEqual(3)
  	})
	});
  });
});

Here, we use the “axios” library to make the HTTP request to the API service. The test scenario is this:

  • First, we get the access token of the "guest.account" user for later calls to the blog's APIs. The access token will be called in the "beforeEach" blog, so with every test, the new token will be used.
  • Then in the test block, we call the API that gets all the blogs from the database. We verify that the statusCode is 200 and the number of blogs is more than three since we already created three blogs before.

Pyramid Figure 13: Successful test run for end-to-end testing with real API

Conclusion

Creating a web application that works is not a difficult job. However, having a quality web application is a completely different story. It requires a lot of hard work and effort from the whole team for designing the software architecture, creating the UI/UX design, and collaborating efficiently to meet all the requirements. And the most important part is testing. Only with thorough testing can we make sure the application works as expected. In this article, we went through the best practices and pitfalls that developers need to deal with when working with NestJS. Now, you can apply these lessons to your current projects or future ones. Happy testing!