How to Test Your Node.js RESTful API with Vitest: Unit & Integration Testing
Learn step-by-step how to add unit and integration tests to your Node.js and Express API using Vitest. Includes clear examples and GitHub repository.
Introduction
In my previous article, we created a foundational Node.js RESTful API using Express.js, MongoDB, and TypeScript.
The API is for a to-do task manager and contains the basic operations you would expect: creating, retrieving, updating, and deleting a task.
You can check it here:
At the end of that article, I listed extra steps to enhance our API and make it more robust.
One critical first enhancement is adding automated tests to ensure the reliability and maintainability of our application.
In this article, we'll explore why tests matter, discuss different types of testing (unit and integration tests specifically), and demonstrate how to implement them using Vitest.
Prerequisites
I will assume the following:
You have a basic understanding of JavaScript/TypeScript.
You have Node.js and npm installed on your local machine.
To follow along, you downloaded the repository from https://github.com/daniosoriov/todo-manager.
Even if you’re newer to testing, I aim to keep explanations straightforward and accessible.
Why are Tests Important?
Tests can be a controversial topic in software development.
Some people hate them, some people love them.
Regardless of your feelings about testing, we must understand the benefits of a well-tested application and why it is essential.
Tests help us:
Catch bugs and errors early during development.
Ensure that our code behaves as expected under different conditions.
Increase maintainability.
Reduce the risk of introducing new issues.
Reduces costs associated with fixing bugs later on.
Enhance the quality of our application and, consequently, the user experience for those who use it.
Identify areas for improvement or optimization.
So, why do some people dislike them if they are so important?
The main reason is time consumption.
Teams with a tight schedule or budget may find testing to be an additional burden rather than a valuable investment, betting that their developers will not make costly mistakes down the road.
However, this is a dangerous assumption because even minor bugs can significantly affect your application's functionality.
Hence, it should always be a priority in any software development project to include thorough initial testing.
What are Automated Tests?
When developing an application and implementing tests, we typically encounter three distinct types of tests:
Unit tests.
Integration tests.
End-to-end (E2E) tests.
I will briefly go through each of them to give you a better idea of what they are and how we will use them in our application.
Unit Tests
As the name suggests, unit tests cover a specific “unit” of our application.
The unit can be a function, middleware, or any other piece of code that can be tested in isolation.
Unit tests help us guarantee that the smaller parts of our code are working correctly.
However, external interactions (like database queries or API calls) are typically mocked, meaning we simulate these external interactions without actually performing them.
The idea is to isolate the code you're testing.
Unit tests typically verify scenarios such as:
Correct behavior when valid data is provided.
Proper handling when mandatory data is missing or invalid.
For example, when creating a task, the unit test should:
Return a successful status code when all the fields required to create a task are provided.
Return an error status if, for example, the title field is empty.
Return an error status if the status is not among “pending”, “in-progress”, or “completed”.
And so on…
With these tests, we can scrutinize the Create Task behaviour and guarantee it is not doing something it shouldn’t be doing.
Integration Tests
Integration tests are a bit more complex than unit tests.
Integration tests verify that multiple components (routes, middleware, database connections, etc.) interact correctly.
Unlike unit tests, integration tests often interact with a real or test database to ensure realistic behavior.
If we continue with our create task example, our integration tests would cover the flow from the initial request, passing through all the middleware on our route, to the final method, creating the task.
For example, to check that a task is successfully created, our integration test should:
Pass through any validation middlewares without errors.
Get to the method that creates the task.
Create the task in the database.
Return the created task.
All of these steps should not fail, and they will demonstrate how the different components of our application interact during task creation.
Integration tests are also used to check scenarios of failure.
For example, the application should not create the task if we forget to pass one of its mandatory fields. And so on…
End-to-end (E2E) Tests
Although we won’t cover end-to-end tests in this article, I want to explain them briefly.
End-to-end (E2E) tests validate your application's workflow, simulating a real user's experience from the frontend interface through backend API interactions and database operations.
Since we do not have a client application in our example, I will skip end-to-end tests, but know that these are also highly important.
Before we continue, I want to discuss the primary tool we will use to create the tests for our application.
What is Vitest?
Vitest is a testing framework that excels in its speed.
Keep reading with a 7-day free trial
Subscribe to Daniel’s Substack to keep reading this post and get 7 days of free access to the full post archives.