
How to Write Tests Using the Node.js Test Runner and mongodb-memory-server
How to Write Tests Using the Node.js Test Runner and mongodb-memory-server 관련
I recently migrated some tests from Jest to the Node.js test runner in two of my projects that use MongoDB. In one of those projects, test runtime was reduced from 107 seconds to 25 seconds (screenshot below). In the other project, test runtime was reduced by about 66%.

I decided to share with you how I was able to implement this. I think you’ll find it helpful, as it’s more cost-effective (in terms of reducing money spent on running tests in CI/CD), and it also improves your developer experience.
Prerequisites
To follow along with this guide, you should have experience working with Node.js, MongoDB, and Mongoose (or any other MongoDB object data mapper). You should also have Node.js (at least v20.18.2) and MongoDB installed on your computer.
The Node.js Test Runner
The Node.js test runner was introduced as an experimental feature in version 18 of Node.js. It became fully available in version 20. It gives you the ability to:
- Run tests
- Report test results
- Report test coverage (still experimental at version 23)
It’s a good idea to use the in-built test runner when writing tests in Node.js because it means that you have to use fewer external dependencies. You don’t need to install an external library (and its peer dependencies) to run tests.
The built-in best runner is also faster. Based on my experience using it on two projects (which formerly used Jest), I saw improvements of at least a 66% reduction in the time taken to run tests completely.
And unlike other testing frameworks or libraries, the Node.js test runner was built specifically for Node.js projects. It doesn’t try to accommodate the specifics of other programming environments like the browser. The specifics of Node.js are its main and only priority.
MongoDB In-Memory Server
For tests that involve making requests to a database, some developers prefer to mock the requests to avoid making requests to a real database. They do this because making a request to a real database requires a lot of setting up which can cost time and resources.
Writing and fetching data using a real database is slower compared to writing and fetching data from memory. When running automated tests, using a real MongoDB server will be slower than using an in-memory database server, and that is where typegoose/mongodb-memory-server
becomes useful.

According to its documentation, mongodb-memory-server creates and starts a real MongoDB server programmatically from within Node.js, but uses an in-memory database by default. It also allows you to connect to the database server it creates using your preferred object data mapper such as Mongoose, Prisma, or TypeORM. In this guide, we’ll use Mongoose (v8.9.6).
Since the data stored by mongodb-memory-server resides in memory by default, it’s faster to read from and write to than when using a real database. mongodb-memory-server is also easier to set up. These benefits make it a good choice for using it as a database server for writing tests.
Note: Make sure to install v9.1.6 of mongodb-memory-server to follow this guide. v10 currently has issues with cleaning up resources after tests are done. See this issue titled Node forking will include any --import
from the original command (typegoose/mongodb-memory-server
).
The issue has been resolved at the time of writing this article, but the fix has not been merged for installs.
How to Write the Tests
Now I’ll take you through the following steps to get you started writing tests:
- Set up the project
- Set up mongoose schema
- Set up services
- Set up tests
- Write tests
- Pass tests
- Use TypeScript (Optional)
1. Set Up the Project
I created a GitHub repository to make it easier for you to follow this guide. Clone the repository at orimdominic/nodejs-test-runner-mongoose
and checkout branch 01-setup
.
In 01-setup
, the dependencies for the project are in the package.json
file. Install the dependencies using the npm install
command to set up the project. To make sure that the setup is complete and correct, run the node .
command in the terminal of your project. You should see your version of Node.js as an output on the terminal.
# install dependencies
npm install
node .
#
# You are running Node.js v22.13.1
2. Set up Mongoose Schema
We’ll set up the schema for two collections (Task and User) in branch 02-setup-schema
(orimdominic/nodejs-test-runner-mongoose
) using Mongoose. The task/
model.mjs
(orimdominic/nodejs-test-runner-mongoose
) and user/
model.mjs
(orimdominic/nodejs-test-runner-mongoose
) files contain the schema for the Task and the User collection, respectively. We’ll also set up a database connection in index.mjs
to ensure that the schema setup works correctly.
I won’t go into detail about Mongoose models and schema in this article because they are outside its scope.
When you run the node .
command after implementing the changes in 02-setup-schema
, you should see a similar result in the console as in the snippet below:
node .
#
# You are running Node.js v22.13.1
# Created user with id 679f1d7f73fbeaf23b2007df
# Created task "Task title" for user with id "679f1d7f73fbeaf23b2007df"
You can see the differences between 01-setup
and 02-setup-schema
via the 01-setup <> 02-setup-schema diff on GitHub (orimdominic/nodejs-test-runner-mongoose
).
3. Set Up Services
Next, we create service files (task/
service.mjs
(orimdominic/nodejs-test-runner-mongoose
) and user/
service.mjs
(orimdominic/nodejs-test-runner-mongoose
)) in branch 03-setup-services
(orimdominic/nodejs-test-runner-mongoose
). Both files currently contain empty functions that we’ll write tests for later. These functions will contain business logic and also communicate with the database. We’re using JSDoc comments for typing parameters and return values.
Click 02-setup-schema <> 03-setup-services diff to see the code changes between 02-setup-schema
and 03-setup-services
.
4. Set Up Tests
In branch 04-set-up-tests
(), we set up the codebase to run tests. We create test.setup.mjs
() which contains code that will be run before each test file is executed.
In test.setup.mjs
, the connect
function creates a MongoDB In-Memory server and connects to it with Mongoose for running the tests. The closeDatabase
function closes the database connection and cleans up all resources to free memory.
The connect
and closeDatabase
functions get executed in the t.before
hook and the t.after
hook respectively. This ensures that, before a test file is run, a database connection is established through t.before
. Then after tests for the file have been completely run, the database connection is dropped and the resources used are cleared up through t.after
.
In package.json
, we’ll update the npm test
script to node --test --import ./test.setup.mjs
. This command ensures that the test.setup.mjs
ES Module is preloaded and executed through the --import
CLI command before each test file is run.
Then we’ll create the test files with empty tests in the __tests__
folders for user
and task
. After implementing the new changes in 04-set-up-tests
(orimdominic/nodejs-test-runner-mongoose
), running the test
script with npm run test
should display output similar to the snippet below:
npm run test
#
# > nodejs-test-runner-mongoose@1.0.0 test
# > node --test --import ./test.setup.mjs
#
# ...
#
# ℹ tests 8
# ℹ suites 5
# ℹ pass 8
# ℹ fail 0
# ℹ cancelled 0
# ℹ skipped 0
# ℹ todo 0
# ℹ duration_ms 941.768873
All tests currently pass because there are no assertions that fail in them. We’ll write tests with assertions in the following section.
5. Write Tests
Now it’s time to write tests for the functions in the service files in the 05-write-tests
(orimdominic/nodejs-test-runner-mongoose
) branch. We’re using the Node.js assert library to ensure that values returned from the functions are what we expect. You can view the tests we’ve written when you compare the differences between 04-set-up-tests
and 05-write-tests
(orimdominic/nodejs-test-runner-mongoose
)
When the tests
script is run, all tests fail because we haven’t written the functions in the service files yet. You should see output similar to the snippet below when you run the test
script:
npm run test
#
# > nodejs-test-runner-mongoose@1.0.0 test
# > node --test --import ./test.setup.mjs
#
# ...
#
# ℹ tests 8
# ℹ suites 5
# ℹ pass 0
# ℹ fail 8
# ℹ cancelled 0
# ℹ skipped 0
# ℹ todo 0
# ℹ duration_ms 1202.031961
6. Pass Tests
In 06-pass-tests
(orimdominic/nodejs-test-runner-mongoose
), we write the functions in the service files to pass the tests. Only 6 out of 7 tests pass when the test
script is run because we skipped the test for the getById
function in user/
service.mjs
has with t.skip
. We haven’t finished the getById
function in user/
service.mjs
. I figured we could leave it as an exercise.
When you run the test
script, you should get a similar output in the terminal as below:
npm run test
# ...
#
# ℹ tests 7
# ℹ suites 4
# ℹ pass 6
# ℹ fail 0
# ℹ cancelled 0
# ℹ skipped 1
# ℹ todo 0
# ℹ duration_ms 1287.564918
You can see the code we wrote to pass tests in the code changes between 05-write-tests
and 06-pass-tests
(orimdominic/nodejs-test-runner-mongoose
).
7. Use TypeScript (Optional)
If you intend to run tests with TypeScript, you can checkout branch 07-with-typescript
(orimdominic/nodejs-test-runner-mongoose
). You need to have Node.js >=v22.6.0
installed because we’re using the --experimental-strip-types
option in the test
. To set up tests to run with TypeScript, go through the following steps:
- Install TypeScript using the
npm install typescript --save-dev
command - Install tsx using the
npm install tsx
command - Create a default
tsconfig.json
file at the root of the project using thenpx tsc --init
command
In package.json
, update the test
script to this:
{
"test": "node --test --experimental-strip-types --import tsx --import ./test.setup.mjs"
}
--experimental-strip-types
helps strip out types before each test file is executed.- Preloading
tsx
with the--import
helps execute the TypeScript file. Without it, the test runner will not be able to find files imported without the.ts
extension. For example,user/
model.ts
imported with the code snippet below will not be found.
import { UserModel } from "./model";
The rest of the changes from 06-pass-tests
to 07-with-typescript
(orimdominic/nodejs-test-runner-mongoose
) involve updating types, changing file extensions from .mjs
to .ts
and updating import statements.
Conclusion
In this guide, you have learned how to use the built-in Node.js test runner and why it’s often a better choice over other testing libraries and frameworks. You have also learned how to use mongodb-memory-server as a replacement for a real MongoDB server, as well as why it’s a good idea to use this instead of a real MongoDB server for tests.
Most importantly, you have learned how to set up and run tests in Node.js using the Node.js test runner and mongodb-memory-server. You should now know how to set up your projects to run the tests if you use TypeScript.
If you find the orimdominic/nodejs-test-runner-mongoose
repository useful, kindly give it a star. It encourages me. Thank you.