Back to Testing guides

A Beginner's Guide to Unit Testing with Japa

Stanley Ulili
Updated on April 24, 2025

Japa is a fast and straightforward testing tool built for Node.js applications. It helps you write and run tests easily, while also allowing you to add more features as needed.

What makes Japa great is its clean design, straightforward way of checking test results, and plugin system that lets you add exactly what you need. It works well for testing Node.js apps of any size.

In this guide, you'll learn how to use Japa's main features and write good tests with it.

Prerequisites

Before you begin, ensure that you have Node.js installed on your computer. Version 20.0 or newer works best.

Step 1 — Setting up the project

Let’s start by creating a project directory and setting up Japa so you can begin testing your JavaScript code.

First, make a new folder and go into it:

 
mkdir japa-demo && cd japa-demo

Next, set up a basic npm project:

 
npm init -y

This creates a package.json file that tracks your project's details and dependencies.

Now install Japa and its testing tools:

 
npm init japa@latest .

Choose the following options:

  • project type: javascript
  • assertion library: @japa/assert
  • additional plugins: none
  • create sample test: false

You’ll see output like this:

Output

....

   ___                             _            
  |_  |                           | |           
    | | __ _ _ __   __ _        __| | _____   __
    | |/ _` | '_ \ / _` |      / _` |/ _ \ \ / /
/\__/ / (_| | |_) | (_| |  _  | (_| |  __/\ V / 
\____/ \__,_| .__/ \__,_| (_)  \__,_|\___| \_/  
            | |                                 
            |_|                                 

❯ Select the project type · javascript
❯ Select the assertion library · @japa/assert
❯ Select additional plugins · No items were selected
❯ Want us to create a sample test? (y/N) · false
DONE:    create bin/test.js
DONE:    update package.json

added 90 packages, and audited 91 packages in 5s

27 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
╭──────────────────────────────────╮
│    Japa setup complete 🧪        │
│──────────────────────────────────│
│                                  │
│    > npm run test                │
│                                  │
╰──────────────────────────────────╯

Japa creates a bin/test.js file and updates your package.json, so you're ready to start writing tests right away.

Set up ES modules in your package.json file:

package.json
{
  . . .
  "scripts": {
    "test": "node bin/test.js"
  },
"type": "module",
... }

We added "type": "module" so you can use modern JavaScript import/export syntax.

Let's create a simple calculator function to test. Make a file called src/calculator.js:

src/calculator.js
export function sum(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

This file has two basic functions: sum adds numbers, and subtract finds the difference between them. Now you're ready to write your first test.

Step 2 — Writing your first test

Unit tests verify that your functions operate correctly in various scenarios. Japa helps you write clear tests that show how your code should work.

Create a tests folder in your project:

 
mkdir -p tests

Now make a file called calculator.spec.js in the tests folder with this content:

tests/calculator.spec.js
import { test } from '@japa/runner'
import { sum, subtract } from '../src/calculator.js'

test.group('Calculator', () => {
  test('sum should add two numbers correctly', ({ assert }) => {
    assert.equal(sum(1, 2), 3)
    assert.equal(sum(5, 7), 12)
    assert.equal(sum(-1, 1), 0)
  })

  test('subtract should subtract second number from first', ({ assert }) => {
    assert.equal(subtract(5, 2), 3)
    assert.equal(subtract(10, 5), 5)
    assert.equal(subtract(1, 5), -4)
  })
})

Here's what this test does:

  • test.group bundles related tests together under the name "Calculator"
  • Each test function describes what specific behavior you expect
  • The assert.equal checks if your functions give the right answers for different inputs

This approach makes your tests easy to understand and shows exactly what your code should do.

Now let's run our tests to see them in action.

Step 3 — Running your tests

Now that you've written your test, let's run it.

Type this command in your terminal:

 
npm test

This runs the test script you added to your package.json file. You should see output like this:

Screenshot of the output

Japa displays a clean report that makes it easy to see which tests ran and whether they passed. The checkmarks (✓) show successful tests.

Let's see what happens when a test fails. Change one line in your test file:

tests/calculator.spec.js
...
test.group('Calculator', () => {
  test('sum should add two numbers correctly', ({ assert }) => {
    assert.equal(sum(1, 2), 3)
    assert.equal(sum(5, 7), 12)
assert.equal(sum(-1, 1), 1) // This is wrong - should be 0
})

Rerun the tests and you'll see:

Screenshot of the failing test

Japa clearly shows which test failed and why, it expected 1 but got 0. This helps you quickly identify and resolve issues.

Fix the test by changing the line back:

tests/calculator.spec.js
...
test.group('Calculator', () => {
  test('sum should add two numbers correctly', ({ assert }) => {
    assert.equal(sum(1, 2), 3)
    assert.equal(sum(5, 7), 12)
assert.equal(sum(-1, 1), 0) // Fixed - now correct
})

Now you know how to run tests and understand the results. Let's look at some more advanced Japa features.

Step 4 — Test setup and cleanup

When writing tests, you often need to prepare things before each test and clean up afterward. Japa has special functions called "lifecycle hooks" that help with this.

Let's create a simple counter to test these hooks. Make a file named src/counter.js:

src/counter.js
export class Counter {
  constructor(initialValue = 0) {
    this.count = initialValue;
  }
  increment() { return ++this.count; }
  decrement() { return --this.count; }
  getValue() { return this.count; }
}

This counter keeps track of a number that you can increase, decrease, or reset.

Now create a test file at tests/counter.spec.js:

tests/counter.spec.js
import { test } from '@japa/runner'
import { Counter } from '../src/counter.js'

test.group('Counter', (group) => {
  let counter

  // Runs before each test
  group.each.setup(() => {
    counter = new Counter()
  })

  test('should start with zero', ({ assert }) => {
    assert.equal(counter.getValue(), 0)
  })

  test('should increment count', ({ assert }) => {
    counter.increment()
    assert.equal(counter.getValue(), 1)
  })
})

This test file uses two important hooks:

  1. group.each.setup() - Creates a fresh counter before each test
  2. group.each.teardown() - Cleans up after each test

These hooks ensure each test starts with a clean counter. This keeps tests independent so they don't affect each other - a key part of good testing.

Run the tests with npm test and you'll see they all pass:

Output
> japa-demo@1.0.0 test
> node bin/test.js


Calculator (tests/calculator.spec.js)
  ✔ sum should add two numbers correctly (0.62ms)
  ✔ subtract should subtract second number from first (0.08ms)

Counter (tests/counter.spec.js)
  ✔ should start with zero (0.07ms)
  ✔ should increment count (0.05ms)

 PASSED 

Tests  4 passed (4)
 Time  3ms 6 tests in 15ms

Japa also has hooks that run just once for all tests in a group:

  • group.setup() - Runs once before any tests in the group
  • group.teardown() - Runs once after all tests finish

These are perfect for things you only need to do once, like connecting to a database:

 
test.group('Database operations', (group) => {
  let db

  // Connect once before all tests
  group.setup(async () => {
    db = await connectToDatabase()
  })

  // Disconnect after all tests finish
  group.teardown(async () => {
    await db.close()
  })

  // Your tests here...
})

Using these hooks makes your tests cleaner, faster, and more reliable.

Step 5 — Testing async code

Modern applications often rely on asynchronous operations, such as API requests, file reading, or database access. With built-in support for Promises and async/await , Japa makes it easy to write tests for this kind of code.

Let's create a service that pretends to fetch user data from an API. Make a file called src/user-service.js:

src/user-service.js
export class UserService {
  async fetchUser(id) {
    // Fake a network delay
    await new Promise(resolve => setTimeout(resolve, 100))

    if (!id) {
      throw new Error('User ID is required')
    }

    // Return fake user data
    return {
      id,
      name: `User ${id}`,
      email: `user${id}@example.com`
    }
  }
}

This service has an async fetchUser method that returns user data after a short delay.

Now create a test file at tests/user-service.spec.js:

tests/user-service.spec.js
import { test } from "@japa/runner";
import { UserService } from "../src/user-service.js";

test.group("UserService", (group) => {
  let userService;

  group.each.setup(() => {
    userService = new UserService();
  });

  test("should fetch user by id", async ({ assert }) => {
    const user = await userService.fetchUser(1);

    assert.equal(user.id, 1);
    assert.equal(user.name, "User 1");
    assert.equal(user.email, "user1@example.com");
  });

  test("should throw error when id is not provided", async ({ assert }) => {
    await assert.rejects(async () => {
      await userService.fetchUser();
    }, "User ID is required");
  });
});

There are two key things to notice in this example. First, the test functions are marked as async, which allows you to use await inside them.

Second, the test uses assert.rejects to verify that the function throws the correct error. When testing asynchronous code in Japa, you have two options: either return the Promise from your test function, or use async and await as shown here.

Japa waits for your async operations to finish before deciding if a test passes or fails.

Run the tests and you'll see they all pass:

Output
...
UserService (tests/user-service.spec.js)
  ✔ should fetch user by id (101.08ms)
  ✔ should throw error when id is not provided (101.57ms)

 PASSED 

Tests  6 passed (6)
 Time  209ms

Notice that the total time is longer (209ms) due to the delay in our fake API calls. Japa correctly waits for these delays before showing the results.

Step 6 — Filtering and organizing tests

As your test suite grows, you'll want to run specific tests rather than the entire suite. Japa offers several ways to filter and organize your tests.

Let's add tags to our calculator tests so we can run them selectively:

tests/calculator.spec.js
import { test } from '@japa/runner'
import { sum, subtract } from '../src/calculator.js'

test.group('Calculator', () => {
  test('sum should add two numbers correctly', ({ assert }) => {
    assert.equal(sum(1, 2), 3)
    assert.equal(sum(5, 7), 12)
    assert.equal(sum(-1, 1), 0)
}).tags(['add', 'unit']);
test('subtract should subtract second number from first', ({ assert }) => { assert.equal(subtract(5, 2), 3) assert.equal(subtract(10, 5), 5) assert.equal(subtract(1, 5), -4)
}).tags(['substract', 'unit'])
})

Now you can run only add-related tests with this command:

 
npm test -- --tags=add

You should see only the calculator tests running:

Output
Calculator (tests/calculator.spec.js)
  ✔ sum should add two numbers correctly (0.61ms)

 PASSED 

Tests  1 passed (1)
 Time  3ms

You can also run tests from specific groups using the --groups flag:

 
npm test -- --groups=Calculator

Focusing on specific tests

When debugging, you might want to focus on just one test temporarily. Japa provides .only and .skip modifiers for this purpose:

 
test.group('Calculator', () => {
test.only('sum should add two numbers correctly', ({ assert }) => {
}) test('subtract should subtract second number from first', ({ assert }) => { // This test will be skipped when using .only above }) })

With this change, Japa will run only the test marked with .only, ignoring all other tests.

Similarly, you can skip specific tests with .skip:

 
test.skip('subtract should subtract second number from first', ({ assert }) => {
// This test will be skipped })

Remember to remove .only and .skip modifiers before committing your code, as they're meant for temporary use during development.

Final thoughts

Japa is a simple yet powerful testing tool for Node.js. In this guide, you learned how to set up a project, write tests, handle async code, use hooks, and filter tests with tags.

Its clean design and flexible features make it great for both small scripts and large applications. As your project grows, Japa scales with you. For more, check out the official docs.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
A Beginner's Guide to Unit Testing with AVA
Learn how to use AVA, a fast and minimal JavaScript test runner, to write and run unit tests with ease. This guide covers setup, async testing, mocking, and code coverage to help you test with confidence.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github