Introduction
Everyone has experienced the need to write integration tests for an application they're working on, which leads to the database becoming clogged with tons of data generated by the test script(s). Even after cleanup, there may still be instances in which our data during development testing interferes with the test resulting in it becoming nondeterministic (we do not want that).
One approach for separating the test environment from the development environment is by using a separate database instance dedicated to testing. However, if there are other services involved, such as caching, message broker, and search engine services, running multiple instances for testing purposes can waste valuable system resources. This becomes even more challenging when working with multiple applications, requiring additional services and implementing in-memory solutions. Overhead and the need for testing these implementations can further complicate the process.
What is Testcontainers?
Testcontainers is a tool built to run services in lightweight docker containers for your tests. In addition to isolating the test environment, it also adds an extra benefit of simulating the test environment as close as possible to that of the production environment. There's also less overhead of writing mocks for the services. Now let us dive into how to use Testcontainers in your Node.js project. We will use Node, NestJs, Jest, Prisma, Postgresql and Testcontainers.
Configuring the Environment
Let us set up a demo to-do application; we will write some tests. Let's run the following commands to install the necessary stuff.
Setting up Database
Now, let us define our database schema in prisma/schema.prisma
. Just add the following content to the file.
Let us set up our Postgres database now. I will be using docker for this. Create a new file in the project root and name it docker-compose.yml. Then copy the following content.
Now open the .env
file and update its content as the following.
Run docker compose up -d
and your database should be up and running.
To sync the database with our schema, run the command :pnpm prisma migrate dev
and name the migration whatever you'd like when prompted.
Now, let's configure our database service. Open the src/prisma.service.ts
file and update the source as following:
Writing Service
Now let us write a simple CRUD service for the to-do app we are going to build. Update the src/todo/todo.module.ts
and add PrismaService
to the list of providers array. Then open the src/todo/todo.service.ts
file and update it as follows:
Writing Helper Functions
Now let us write some helper functions to set up testcontainers. Create a new file utils/test.utils.ts
and populate the content as follows:
The above functions will create a database container and create a Prisma service that connects to the container during the test. It will also apply migrations to the testcontainer database.
Now coming back to todo.service.spec.ts
we will be overriding the Prisma service with our own Prisma service that connects to the test container. Override the beforeAll
function with the following piece of code.
What the above piece of code does is override the PrismaService
with the PrismaService
instance generated by setupPrismaService
which is connected to the database in the test container.
Writing Actual Tests
Writing tests are straightforward. As stated earlier, there won't be much mocking involved. Let's write a simple test to insert a to-do entity in the database.
Now run the test with the command pnpm test
and we should see that the test passes.
Now let's write a few more tests for the remaining CRUD operations
Upon running the command pnpm test
we should find the following result.
By doing all this, we have completely isolated the dev database environment and test database environment. Testcontainers provides containers for 50+ common services such as Redis, Elasticsearch, KeyCloak, MinIO, Nginx, and many more.
Conclusion
In this blog, we learned how to isolate the development environment and testing environment with the help of a tool called Testcontainers. This brings many benefits to the table, such as
- Tests are more deterministic.
- Tests can be run on an environment that closely replicates production.
- Less overhead writing mocks or in-memory implementations of external services.
- Don't have to worry about test data cleanup since the tests are run in temporary environments.
Thank you for reading this article. Please consider subscribing or leaving a comment if you loved it.