Testing applications can seem like a fairly complicated concept, and thus, many programmers avoid it due to the fear of failure — especially in the Node.js world, where testing applications are not so ubiquitous as in, say, Java, and the resources on testing are scarce.
Although getting an application tested thoroughly is a complicated thing to do, unit testing is the easiest kind of testing to grasp. As its name suggests, unit testing focuses on writing tests for the smallest possible units. A unit is often a single function or method. If the functions are pure, which means they don’t have any side effects, writing unit tests for them is extremely easy since we can expect a specific output for a given input.
expect(add(2, 2)).toBe(4);
Now, we can’t really assume that every function in our codebase is going to be pure. So the tests will get more complicated than simply calling a function and seeing whether the output is what we expect.
Usually, our functions internally make use of other functions. For example, there might be a method called userService.createUser()
, which would internally call userRepository.create()
to create the user entity and userRepository.save()
to save it in the database of choice. In this case, testing the output would be silly at best and wouldn’t really give us any confidence in whether the code is working as expected.
Unit tests are only interested in the logic that the function itself contains, not the external one.
So in this example, the test would check whether userRepository.create()
has been called and, if so, with what arguments. Isolating the logic from all the external dependencies is called mocking. It means replacing all the specific implementations with fake ones that are monitored by the testing environment. This way, tracking the calls that have been made to a certain external method or overriding its return value is a breeze.
The thing about unit tests is that they shouldn’t be dependent on the environment in which they are being run, and they are supposed to be fast. Just to give you an example of the magnitude of speed you’d expect here: unit tests should be run after each commit. Of course, there are many techniques, usually based on Git, that allow us to run only the tests that have been themselves changed or that depend on the files that have been changed.
End-to-end (E2E) tests would be an example of those that are dependent on their environment. In E2E tests, we test an application as a whole, so we provide a test database and prepare the expected environment. But it is slow and needs a special setup, and thus, it is not really feasible to be run during development, which is when we usually run unit tests. There should be a specialized setup on a CI/CD service that would take care of running E2E tests.
Controlling external dependencies has one very important downside, though. In the test, we override the value that would normally be returned by the dependency. If, for whatever reason, the external dependency changes and now returns a { token, user }
object instead of a User
object, the tests would still pass because it would still return a User
object in our test environment.
That is why unit testing is only one of many kinds of tests. It ensures that the function has the expected behavior when we control all the external dependencies. There are also, for example, integration tests and contract tests that can assure us the contracts (expected input/output values of the external dependencies) haven’t changed.
But let’s be honest, it’s not like we change our methods every other day. Most of the code never changes, or at least the I/O values don’t, so by starting with unit tests only, we can still have much better confidence in the code that we write.
Nest forces us to write more easily testable code through its built-in dependency injection — a design pattern that states that a central authority is taking care of creating and supplying dependencies, while the classes simply assume that the dependency will be provided instead of creating it themselves. So, instead of writing:
constructor() { this.a = new A(); }
We would write:
constructor(private readonly a: A) {}
The latter syntax is possible due to the JS Metadata Reflection API that was proposed not so long ago.
Dependency injection is usually based on interfaces rather than concrete classes. In TypeScript, however, interfaces (and types in general) are only available during compile time rather than runtime, and thus can’t be relied upon afterwards. So instead of using interfaces, in Nest it is common to use class-based injection.
With dependency injection, replacing our dependencies becomes trivial. But with unit tests, we would rather not recreate the whole application for each test. Instead, we would like to create the bare minimum setup. Thankfully, Nest provides us with the @nestjs/testing
package. The package enables us to create a Nest module, like we normally would, by declaring only the dependencies used in the tests. Here’s an example:
const module: TestingModule = await Test.createTestingModule({ providers: [ PlaylistService, { provide: getRepositoryToken(Playlist), useClass: PlaylistRepositoryFake, }, ], }).compile(); playlistService = module.get(PlaylistService); playlistRepository = module.get(getRepositoryToken(Playlist));
So in the example above, we “import” to the testing module two things: playlistService
and playlistRepository
. This setup would be ideal to test things like creating a playlist; we would check whether the repository is called with the expected parameters.
Hopefully, by now you understand why Nest’s codebases are easily testable and what’s going on behind the scenes. Now let’s take a look at a practical example: testing the aforementioned playlist module scenario.
The repository contains the code that will be used later on in the examples. Bear in mind that this is a very basic setup that doesn’t necessarily reflect how an application should be structured.
We are going to focus on the PlaylistService
class that is responsible for creating, finding, updating, and removing playlists. In a more sophisticated setup, we would probably create separate classes for each case (one for creating, one for updating) and their related logic, but we are going to keep it simple.
@Injectable() export class PlaylistService { public constructor( @InjectRepository(Playlist) private readonly playlistRepository: Repository<Playlist>, ) {} public async findOneByIdOrThrow(id: string): Promise<Playlist> { const playlist = await this.playlistRepository.findOne({ id, }); if (!playlist) { throw new NotFoundException('No playlist found.'); } return playlist; } public async createOne( createPlaylistData: CreatePlaylistData, ): Promise<Playlist> { const { title, description } = createPlaylistData; const playlist = this.playlistRepository.create({ title, description, }); const createdPlaylist = await this.playlistRepository.save(playlist); return createdPlaylist; } public async removeOne( removePlaylistData: RemovePlaylistData, ): Promise<void> { const { id } = removePlaylistData; const playlist = await this.findOneByIdOrThrow(id); await this.playlistRepository.remove([playlist]); return null; } public async updateOne( updatePlaylistData: UpdatePlaylistData, ): Promise<Playlist> { const { id, ...updateData } = updatePlaylistData; const existingPlaylist = await this.findOneByIdOrThrow(id); const playlist = this.playlistRepository.create({ ...existingPlaylist, ...updateData, }); const updatedPlaylist = await this.playlistRepository.save(playlist); return updatedPlaylist; } }
All of the methods are the most basic implementation of the CRUD behavior. Essentially, they are calling their respective repository methods, so the .createOne()
method calls playlistRepository.save()
, the .findOne()
method calls playlistRepository.findOne()
, etc.
With the implementation in place, let’s test it.
By default, the projects generated with the Nest CLI are set up to run tests with Jest, so we are going to stick with it.
Writing unit tests is easy once you get the hang of it. It is really just creating scenarios in which the method would behave differently and then testing its expected behavior.
Before writing the tests, let’s create the testing module.
describe('PlaylistService', () => { let playlistService: PlaylistService; let playlistRepository: Repository<Playlist>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PlaylistService, { provide: getRepositoryToken(Playlist), useClass: PlaylistRepositoryFake, }, ], }).compile(); playlistService = module.get(PlaylistService); playlistRepository = module.get(getRepositoryToken(Playlist)); }); });
Just as in the example above, the module will create the playlistService
and playlistRepository
and resolve their dependencies. Since we don’t want to create an actual database connection, we are overriding the playlistRepository
with PlaylistRepositoryFake
. The getRepositoryToken
function lets us get the injection token of the repository.
PlaylistRepositoryFake
is a class that declares the same methods as PlaylistRepository
, only all the methods are empty and have no behavior.
export class PlaylistRepositoryFake { public create(): void {} public async save(): Promise<void> {} public async remove(): Promise<void> {} public async findOne(): Promise<void> {} }
You might be wondering why we have to do that — won’t we be mocking all the external dependencies?
The fakes have two important roles:
The first reason to use fakes is especially important in our example: a class, playlistRepository
, that has some specific logic executed during the creation of an instance.
The repository, when created, expects the database module to be imported (in the module context) and the connection to the database to be open. Since we are not importing the database module and there is no database running at all, the repository would throw an error during creation of the module.
By using a whole other class (the fake), all the logic related to the database module is gone. It is a simple object that does nothing.
In order to check that a method actually made use of an external dependency, we have to somehow register that the call has been made. For example, as discussed before, we would like to be sure that the .createOne()
method actually called playlistRepository.save()
and saved the entity in the database.
In the testing module setup, we save the references to each created instance, so playlistRepository
and playlistService
each have references to the objects instantiated by the testing module.
playlistService = module.get(PlaylistService); playlistRepository = module.get(getRepositoryToken(Playlist));
Since objects in JavaScript are passed around by reference, we can easily override a specific method, like:
let calledTimes = 0 playlistRepository.save = () => { calledTimes += 1; }
But doing so by hand would be tiresome and error-prone. Jest provides us with a better way to do that: spies.
Spies are defined in the following manner:
const playlistRepositorySaveSpy = jest .spyOn(playlistRepository, 'save') .mockResolvedValue(savedPlaylist);
This spy does two things: it overrides both the .save()
method of playlistRepository
and provides an API for developers to choose what should be returned instead. In this snippet, we use .mockResolvedValue()
as opposed to .mockReturnValue()
due to the original implementation returning a promise.
By keeping a reference to the spy (assigning it to a variable), we can later on, in the test, do the following:
expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity);
Note that the variable name consists of three parts: first, the name of the object that is being overriden; second, the method’s name; and third, the word Spy
, so we can easily distinguish it from our other variables. Keeping the variables’ names structured like that helped me personally to keep my tests cleaner and more readable overall.
Although we are overriding the behavior of a method, Jest’s spies still require the provided object to have said property. So if we provided a simple {}
empty object, Jest would throw the following error:
Cannot spy the updateOne property because it is not a function; undefined given instead
I have decided to name the replacement class as “Fake” because, to the best of my knowledge, that is the appropriate name for an object that contains the simplified logic of a class, or a total lack thereof.
People often get confused as to the difference between a fake and a test double, and all the other names thrown around in the testing world. Here’s a great article explaining the different names used in tests and their meanings.
Note that in Jest, spies are mocks and can also be stubs since they are registering the calls (mock) and can override the returned value (stub).
With all the jargon out of the way, let’s write some tests.
.createOne()
methodLet’s quickly take a look at the .createOne()
method implementation:
public async createOne( createPlaylistData: CreatePlaylistData, ): Promise<Playlist> { const { title } = createPlaylistData; if (!title) { throw new BadRequestException('Title is required.'); } const playlist = this.playlistRepository.create({ title, }); const createdPlaylist = await this.playlistRepository.save(playlist); return createdPlaylist; }
As a parameter, the method takes an object that holds the title of the playlist. If the title isn’t there, the method throws an error. That’s the first case we’ll test for.
describe('creating a playlist', () => { it('throws an error when no title is provided', async () => { expect.assertions(2); try { await playlistService.createOne({ title: '' }); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); expect(e.message).toBe('Title is required.'); } }); });
I like to group tests in logical blocks that describe the action taking place — so, in this case, creating a playlist.
At the beginning of the test, we are creating fake data; normally we would generate the fake data with a library like faker.js, but here, the only thing we need is an empty string. Since we know the method is going to throw early — due to the title not being provided — we don’t have to spy on any external method because it will never be called in this case.
After we call the function with the faked arguments, we expect it to throw an error. Here, we should be specific. We shouldn’t expect any kind of error to be thrown, because it may as well be a typo in our code that would throw the error. It is better to expect that the error is, for example, an instance of a specific class. In our case, that would be a BadRequestException
.
After calling Jest’s .expect(value)
method, an object containing Jest’s matches is returned. Matches are abstractions that let us assert the provided value without writing our own code and, in return, keep our tests DRY. Using the matchers significantly shortens the test code and improves readability.
An example of a matcher would be the .toEqual()
method that checks whether two objects are the same. It does so by comparing their keys and values recurrently, whereas Jest’s .toBe()
method would simply check the strict equality using the ===
operator.
Check the documentation on Jest’s built-in matchers for more.
The first scenario is done. Let’s move on to the so-called happy path in which everything goes as planned.
describe('creating a playlist', () => { it('throws an error when no title is provided', async () => { // ... }); it('calls the repository with correct paramaters', async () => { const title = faker.lorem.sentence(); const createPlaylistData: CreatePlaylistData = { title, }; const createdPlaylistEntity = Playlist.of(createPlaylistData); const savedPlaylist = Playlist.of({ id: faker.random.uuid(), createdAt: new Date(), updatedAt: new Date(), title, }); const playlistRepositorySaveSpy = jest .spyOn(playlistRepository, 'save') .mockResolvedValue(savedPlaylist); const playlistRepositoryCreateSpy = jest .spyOn(playlistRepository, 'create') .mockReturnValue(createdPlaylistEntity); const result = await playlistService.createOne(createPlaylistData); expect(playlistRepositoryCreateSpy).toBeCalledWith(createPlaylistData); expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity); expect(result).toEqual(savedPlaylist); }); });
One thing you might notice is that the setup is fairly similar to the one in the previous scenario. The setup will always look somewhat the same because of the AAA (arrange, act, assert) pattern. The pattern is rather self-explanatory, but it suggests doing things in the specified order.
First, we set up all the fake data needed for the test. Then, we call the appropriate piece of code (in our case, the method) and we use Jest’s .expect()
function to assert that the results are those we expected. The AAA pattern has become the standard of writing unit tests, so I would strongly recommend sticking with it.
Now, before we call the method with the fake arguments, we create the spies. In this case, we need spies for two external methods: playlistRepository.create()
and playlistRepository.save()
, both of which we mock (to track the calls) and stub (to override the returned value).
After we call the method, we expect that the result is the playlist that was “saved” in the database.
In order to check that the mocks have been called, we can use the expect(mock).toHaveBeenCalledWith()
method and provide the arguments with which we expected the mocks to have been called.
After writing these two test scenarios, the one with the error and the happy path, we can now be sure that, assuming the dependencies work exactly like their contracts (the provided mocks and stubs), the .createOne()
method works correctly.
Given that each test follows the AAA pattern, there’s no point in repeating the explanation for each and every one of them.
Tests for the three remaining methods — .updateOne()
, .findOne()
, and .removeOne()
— look more or less the same.
describe('PlaylistService', () => { let playlistService: PlaylistService; let playlistRepository: Repository<Playlist>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PlaylistService, { provide: getRepositoryToken(Playlist), useClass: PlaylistRepositoryFake, }, ], }).compile(); playlistService = module.get(PlaylistService); playlistRepository = module.get(getRepositoryToken(Playlist)); }); describe('updating a playlist', () => { it('calls the repository with correct paramaters', async () => { const playlistId = faker.random.uuid(); const title = faker.lorem.sentence(); const updatePlaylistData: UpdatePlaylistData = { id: playlistId, title, }; const existingPlaylist = Playlist.of({ id: playlistId, createdAt: new Date(), updatedAt: new Date(), title: faker.lorem.word(), }); const newPlaylistData = Playlist.of({ ...existingPlaylist, title, }); const savedPlaylist = Playlist.of({ ...newPlaylistData, }); const playlistServiceFindOneByIdOrThrowSpy = jest .spyOn(playlistService, 'findOneByIdOrThrow') .mockResolvedValue(existingPlaylist); const playlistRepositoryCreateSpy = jest .spyOn(playlistRepository, 'create') .mockReturnValue(newPlaylistData); const playlistRepositorySaveSpy = jest .spyOn(playlistRepository, 'save') .mockResolvedValue(savedPlaylist); const result = await playlistService.updateOne(updatePlaylistData); expect(playlistServiceFindOneByIdOrThrowSpy).toHaveBeenCalledWith( updatePlaylistData.id, ); expect(playlistRepositoryCreateSpy).toHaveBeenCalledWith({ ...existingPlaylist, title, }); expect(playlistRepositorySaveSpy).toHaveBeenCalledWith(newPlaylistData); expect(result).toEqual(savedPlaylist); }); }); describe('removing a playlist', () => { it('calls the repository with correct paramaters', async () => { const playlistId = faker.random.uuid(); const removePlaylistData: RemovePlaylistData = { id: playlistId, }; const existingPlaylist = Playlist.of({ id: playlistId, createdAt: new Date(), updatedAt: new Date(), title: faker.lorem.sentence(), }); const playlistServiceFindOneByIdOrThrowSpy = jest .spyOn(playlistService, 'findOneByIdOrThrow') .mockResolvedValue(existingPlaylist); const playlistRepositoryRemoveSpy = jest .spyOn(playlistRepository, 'remove') .mockResolvedValue(null); const result = await playlistService.removeOne(removePlaylistData); expect(playlistServiceFindOneByIdOrThrowSpy).toHaveBeenCalledWith( removePlaylistData.id, ); expect(playlistRepositoryRemoveSpy).toHaveBeenCalledWith([ existingPlaylist, ]); expect(result).toBe(null); }); }); describe('creating a playlist', () => { it('throws an error when no title is provided', async () => { const title = ''; expect.assertions(2); try { await playlistService.createOne({ title }); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); expect(e.message).toBe('Title is required.'); } }); it('calls the repository with correct paramaters', async () => { const title = faker.lorem.sentence(); const createPlaylistData: CreatePlaylistData = { title, }; const createdPlaylistEntity = Playlist.of(createPlaylistData); const savedPlaylist = Playlist.of({ id: faker.random.uuid(), createdAt: new Date(), updatedAt: new Date(), title, }); const playlistRepositorySaveSpy = jest .spyOn(playlistRepository, 'save') .mockResolvedValue(savedPlaylist); const playlistRepositoryCreateSpy = jest .spyOn(playlistRepository, 'create') .mockReturnValue(createdPlaylistEntity); const result = await playlistService.createOne(createPlaylistData); expect(playlistRepositoryCreateSpy).toBeCalledWith(createPlaylistData); expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity); expect(result).toEqual(savedPlaylist); }); }); describe('finding a playlist', () => { it('throws an error when a playlist doesnt exist', async () => { const playlistId = faker.random.uuid(); const playlistRepositoryFindOneSpy = jest .spyOn(playlistRepository, 'findOne') .mockResolvedValue(null); expect.assertions(3); try { await playlistService.findOneByIdOrThrow(playlistId); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); expect(e.message).toBe('No playlist found.'); } expect(playlistRepositoryFindOneSpy).toHaveBeenCalledWith({ id: playlistId, }); }); it('returns the found playlist', async () => { const playlistId = faker.random.uuid(); const existingPlaylist = Playlist.of({ id: playlistId, createdAt: new Date(), updatedAt: new Date(), title: faker.lorem.sentence(), }); const playlistRepositoryFindOneSpy = jest .spyOn(playlistRepository, 'findOne') .mockResolvedValue(existingPlaylist); const result = await playlistService.findOneByIdOrThrow(playlistId); expect(result).toBe(existingPlaylist); expect(playlistRepositoryFindOneSpy).toHaveBeenCalledWith({ id: playlistId, }); }); }); });
Each test first generates the fake data, then sets up spies, executes the method, and expects the result and the mocks to be a certain way.
Nest’s CLI-generated projects are automatically configured to run tests by simply typing npm run test
in the terminal. All in all, with Jest, running the test usually comes down to executing the Jest
command.
During development, it is really helpful to have the tests run after each change to the code, so Jest also provides us with the --watch
option to do just that. More on setting up Jest with a more case-oriented config can be found in the docs.
Unit testing our application is the first step to creating more resilient and robust software. Running said tests on each commit (locally) and on a CI service can greatly improve our confidence.
Thanks to Nest — or, generally speaking, any kind of framework that makes use of dependency injection — code is easily testable thanks to all of its dependencies being managed outside the implementation, which, in turn, makes them easily replaceable.
Having unit tests in place also helps during refactoring because we are sure we didn’t break anything while trying to improve our codebase.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
6 Replies to "Unit testing NestJS applications with Jest"
I don’t seem to have the .of method on my entity class. I see you use Playlist.of to create an object to test against. What am I missing?
Thanks for catching that Lucas. The .of() method is simply used to create an instance of the class. You can see it for yourself here: https://github.com/maciejcieslar/nest-unit-tests/blob/master/src/app/playlist/playlist.entity.ts
About the Playlist.of method yet, lets say we extend Playlist from a BaseEntity, could we implement it there? If so, how would we deal with the types? Thanks
This is a great article and would have been better if there was a sample Github repository to see all the code examples at once.
Hey Gbols, the repository is linked in the “Real-world example” subsection: https://github.com/maciejcieslar/nest-unit-tests
This is a great article to know the concept of testing, and how to do it with the nest.
It helps me a lot to do testing.
Thank you so much for sharing your knowledge.