# How to Run the Node Test Runner in Next.js for Beginners

> How to run the Node test runner in Next.js for beginners: write a first test, read a failure, test an API handler, then gate CI with npm test.

URL: https://www.matthewswong.com/en/blog/cara-menjalankan-node-test-runner-nextjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: Web Dev

> **Key takeaway:** How to run the Node test runner in Next.js: write a file named *.test.js that imports describe and it from node:test and assert from node:assert/strict, run it with node --test, then add npm test as a GitHub Actions step before the build. Exit code 1 on a failing test stops the pipeline, no Jest needed.

You change one small function, the build is green, and a week later you notice it returns the wrong value for one edge case. Nothing caught it because there was no test. The usual next move is hunting for a test framework, and that is where beginners stall: Jest, Vitest, config files, transformers. Node.js already ships its own test runner.

This article takes you from an empty folder to a first test that runs, fails on purpose, tests an API handler and then becomes a gate in GitHub Actions. Every command was actually run and the output is copied as it appeared. In this site's repo, tests really do live in a tests/ folder and run with one npm script, with no test framework.

Want to know when a rule should be a test and when a build gate? Take the free lesson

Test a Next.js Site With node:test and Build Gates

. This article is its hands-on version, step by step.

## What you need

Three things, and none of them is an npm install:

Node.js 20 or newer. The Node documentation marks the test runner stable since v20.0.0.

A project folder with a package.json. For a Next.js project, your existing folder is enough.

A terminal, and a GitHub repo if you want to reach the last step.

## Step 1: Set up the folder and the npm test script

Create a small practice folder (or use your Next.js project, only the file locations differ). The type module line lets .js files use import, and the test script just calls node --test with no argument, so Node finds test files by name on its own.

The naming rule matters and comes back in the error table. With no argument, Node runs files matching patterns like **/*.test.js, **/*-test.js, **/test-*.js, or any file inside a folder named test. A folder named tests is not picked up by itself, so name your file *.test.js.

## Step 2: Write a pure function and your first test

Start with a pure function: input in, output out, no network and no database. That is the cheapest kind of code to test. The slugify function below turns a title into a URL segment.

Look at the cases table. One loop turns each row into its own named test, so adding an edge case is one line of data, and a failure names the exact input that broke. The repo's Accept-header tests use the same pattern. Import assert from node:assert/strict so assert.equal compares strictly like strictEqual, not with ==.

## Step 3: Run it, then break it on purpose

Run npm test and read the output. Then change one expected value to something wrong (for example hello_world), run it again, and notice three things: the cross on the failing test, the actual and expected parts, and exit code 1. That exit code is what GitHub Actions will read later.

Reading a failure matters as much as writing a test. The plus sign is the real value from your function, the minus sign is what you expected. If you swap them in your head, you will fix the wrong side.

## Step 4: Test an API handler that returns a Response

A route handler does not have to be tested through a real server. Move its logic into a function that returns a Response, then call that function directly. Response.json builds a Response object with a JSON body, and you can read its status, headers and body. This repo does something similar: a test calls the methodNotAllowed helper and checks the 405 status, the Allow header and the code in the JSON body.

Because response.json returns a promise, the test must be async and use await. Forgetting await is the most common mistake in this step, and the message is confusing: actual is undefined, not a promise.

Anything that depends on real HTTP, such as which headers a deployed server actually sends, does not belong in a test like this. This repo keeps that kind of check in a separate script that probes the running site.

## Step 5: Run part of the suite and see how this repo does it

Once you have many tests, run only some with --test-name-pattern. The pattern is interpreted as a JavaScript regular expression and matched against test names. There is also --watch to rerun when files change, but the Node documentation marks it as still experimental.

In this site's repo, the test script calls node --test on the tests/ folder, which holds files such as agent-readiness.test.mjs. Tests that import TypeScript modules load a small resolver first, and the file header notes that this needs Node 22.18 or newer. The Node documentation records that type stripping is enabled by default since v22.18.0. For plain JavaScript tests like the ones in this article, Node 20 is enough.

## Step 6: Make npm test a gate in GitHub Actions

A test that only runs when you remember to run it will get skipped. Add it as a workflow step before the build, so a pull request with a red test never reaches the build or deploy stage. A run step fails when its exit code is non-zero, and that is the whole mechanism.

For comparison, the verify job in this site's deploy workflow runs npx tsc --noEmit and npm run lint, and the build job waits for it through needs. Adding npm test between them is one line of the same shape.

No workflow yet? First follow

how to build a CI/CD pipeline with GitHub Actions for beginners

, then come back here to add the test step.

> A test whose name does not match the pattern does not error, it just does not run. If the tests count in the output is lower than you expected, check the file name before you suspect the code.

## Common errors and how to fix them

Six problems that most often appear the first time you use node:test, with their causes and fixes.

| Symptom | Cause | Fix |
| --- | --- | --- |
| ERR_MODULE_NOT_FOUND: Cannot find module .../lib/slugify, exit code 1 | A relative import in an ES module without the file extension. | Write it in full: import from ../lib/slugify.js, not ../lib/slugify. |
| The tests count is lower than it should be, with no error | The file name does not match the pattern, for example tests/slugify.js, so it never runs. | Rename it to slugify.test.js, or pass a glob pattern as an argument to node --test. |
| An async test fails with actual being undefined | A missing await on response.json(), so a promise is compared instead of its content. | Make the test async and write await response.json(). |
| assert.equal passes when 1 is compared with the text 1 | Importing from plain node:assert uses legacy mode, which compares with ==. | Import from node:assert/strict, where assert.equal is an alias of strictEqual. |
| Tests pass on your machine but fail importing a TypeScript file on an old Node | Node below 22.18 does not enable type stripping by default. | Raise the Node version locally and in the workflow, or write tests for plain JavaScript files. |
| The pipeline is green but the tests never ran | The workflow has no npm test step, or package.json has no test script. | Add run: npm test before the build and make sure the test script calls node --test. |

## Checklist before you push

Test files end in .test.js or .test.mjs and show up in the tests count.

You broke one test on purpose and saw exit code 1.

Async tests use await for every promise.

assert is imported from node:assert/strict.

The workflow runs npm ci, then npm test, then npm run build.

## Go deeper

The free lesson

explains when a rule becomes a test and when it becomes a build gate that fails the build. For the full pipeline through to deploy, read

, and to test the built app in a real browser, see

.

The rule to carry away: start from a pure function, write the cases as a data table, break one value to prove the test can actually fail, then put npm test before the build so exit code 1 guards production instead of your memory.

## Sources and references

- Node.js docs: Test runner (node:test, node --test, file name patterns, exit code)
- Node.js docs: Assert, strict mode and assert.equal
- Node.js docs: Running TypeScript natively (type stripping)
- GitHub docs: Building and testing Node.js in Actions
- MDN: the static Response.json()

## Source links

- https://nodejs.org/api/test.html
- https://nodejs.org/api/assert.html
- https://nodejs.org/api/typescript.html
- https://docs.github.com/en/actions/tutorials/build-and-test-code/nodejs
- https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static

## FAQ

### Can I run tests in Next.js without Jest or Vitest?

Yes. Node.js ships a built-in test runner, and the documentation marks it stable since v20.0.0. You write files named like *.test.js that import describe and it from node:test, then run node --test. It works best for pure functions and for handlers you can call directly.

### Why does node --test not find my test file?

With no arguments, Node only runs files that match its default patterns, such as *.test.js, *-test.js, test-*.js, or files inside a folder named test. A file called tests/slugify.js matches none of them, so it is silently skipped. Rename it to slugify.test.js or pass a glob pattern.

### Why does my async test fail with actual undefined?

Most likely you forgot await on a promise, such as response.json(). The assertion then compares a promise, not the parsed body. Make the test function async and write await before the call.

### How do I test a Next.js API route handler?

Move the logic into a function that returns a Response, then call it directly in a test and check the status, headers and body. You can avoid starting a server entirely. Checks that need real HTTP behaviour, such as headers from a deployed server, belong in a separate script.

### How do I make GitHub Actions fail when a test fails?

Add a run step with npm test after npm ci and before the build. node --test sets exit code 1 when any test fails, and a workflow step with a non-zero exit code fails the job. Later jobs that depend on it through needs then never start.

---

- HTML version: https://www.matthewswong.com/en/blog/cara-menjalankan-node-test-runner-nextjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cara-menjalankan-node-test-runner-nextjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
