Skip to content

privatenumber/manten

Repository files navigation

満点 (manten)

Lightweight testing library for Node.js

Features

  • Minimal API: test, describe, testSuite
  • Async first design
  • Flow control via async/await
  • Strongly typed
  • Tiny! 2.3 kB

Premium sponsor banner

Install

npm i -D manten

Quick start

  • All test files are plain JavaScript files
  • test()/describe() run on creation
  • Use async/await/Promise API for async flow control
  • Nest describe() groups by inheriting a new the describe function
  • expect assertion library is re-exported
  • When a test fails, the Node.js process will exit with code 1
// tests/test.mjs

import { describe, expect } from 'manten'

describe('My test suite', ({ test, describe }) => {
    test('My test', () => {
        expect(true).toBe(true)
    })

    describe('Nested groups', ({ test }) => {
        // ...
    })
})

Run the test file with Node.js:

node tests/test.mjs

Usage

Writing tests

Create and run a test with the test(name, testFunction) function. The first argument is the test name, and the second is the test function. Optionally, you can pass in a timeout in milliseconds as the third argument for asynchronous tests.

The test runs immediately after the test function is invoked and the results are logged to stdout.

import { test, expect } from 'manten'

test('Test A', () => {
    expect(somethingSync()).toBe(1)
})

// Runs after Test A completes
test('Test B', () => {
    expect(somethingSync()).toBe(2)
})

Assertions

Jest's expect is exported as the default assertion library. Read the docs here.

import { expect } from 'manten'

Feel free to use a different assertion library, such as Node.js Assert or Chai.

Grouping tests

Group tests with the describe(description, testGroupFunction) function. The first parameter is the description of the group, and the second is the test group function.

Note, the test function is no longer imported but is passed as an argument to the test group function. This helps keep track of async contexts and generate better output logs, which we'll get into later.

import { describe } from 'manten'

describe('Group description', ({ test }) => {
    test('Test A', () => {
        // ...
    })

    test('Test B', () => {
        // ...
    })
})

describe() groups are infinitely nestable by using the new describe function from the test group function.

describe('Group description', ({ test, describe }) => {
    test('Test A', () => {
        // ...
    })

    // ...

    describe('Nested group', ({ test }) => {
        // ...
    })
})

Asynchronous tests

Test async code by passing in an async function to test().

Control the flow of your tests via native async/await syntax or Promise API.

Sequential flow

Run asynchronous tests sequentially by awaiting on each test.

Node.js v14.8 and up supports top-level await. Alternatively, wrap the whole block in an async IIFE.

await test('Test A', async () => {
    await somethingAsync()
})

// Runs after Test A completes
await test('Test B', async () => {
    await somethingAsync()
})

Concurrent flow

Run tests concurrently by running them without await.

test('Test A', async () => {
    await somethingAsync()
})

// Runs while "Test A" is running
test('Test B', async () => {
    await somethingAsync()
})

Timeouts

Pass in the max time duration a test can run for as the third argument to test().

test('should finish within 1s', async () => {
    await slowTest()
}, 1000)

Grouping async tests

All descendant tests in a describe() are collected so awaiting on the describe() will wait for all async tests inside to complete, even if they are not individually awaited.

To run tests inside describe() sequentially, pass in an async function to describe() and use await on each test.

await describe('Group A', ({ test }) => {
    test('Test A1', () => {
        // ...
    })

    // Test A2 will run concurrently with A1
    test('Test A2', () => {
        // ...
    })
})

// Will wait for all tests in Group A to finish
test('Test B', () => {
    // ...
})

Test suites

Group tests into separate files by exporting a testSuite(). This can be useful for organization, or creating a set of reusable tests since test suites can accept arguments.

// test-suite-a.ts

import { testSuite } from 'manten'

export default testSuite((
    { describe, test },

    // Can have parameters to accept
    value: number
) => {
    test('Test A', async () => {
        // ...
    })

    describe('Group B', ({ test }) => {
        // ...
    })
})
import testSuiteA from './test-suite-a'

// Pass in a value to the test suite
testSuiteA(100)

Nesting test suites

Nest test suites with the describe() function by calling it with runTestSuite(testSuite). This will log all tests in the test suite under the group description.

import { describe } from 'manten'

describe('Group', ({ runTestSuite }) => {
    runTestSuite(import('./test-suite-a'))
})

Hooks

Test hooks

onTestFail

By using the onTestFail hook, you can debug tests by logging relevant information when a test fails.

test('Test', async ({ onTestFail }) => {
    const fixture = await createFixture()
    onTestFail(async (error) => {
        console.log(error)
        console.log('inspect directory:', fixture.path)
    })

    throw new Error('Test failed')
})
onTestFinish

By using the onTestFinish hook, you can execute cleanup code after the test finishes, even if it errors.

test('Test', async ({ onTestFinish }) => {
    const fixture = await createFixture()
    onTestFinish(async () => await fixture.remove())

    throw new Error('Test failed')
})

Describe hooks

onFinish

Similarly to onTestFinish, you can execute cleanup code after all tests in a describe() finish.

describe('Describe', ({ test, onFinish }) => {
    const fixture = await createFixture()
    onFinish(async () => await fixture.remove())

    test('Check fixture', () => {
        // ...
    })
})

Premium sponsor banner

Examples

Testing a script in different versions of Node.js

import getNode from 'get-node'
import { execaNode } from 'execa'
import { testSuite } from 'manten'

const runTest = testSuite((
    { test },
    node: { path: string; version: string }
) => {
    test(
        `Works in Node.js ${node.version}`,
        () => execaNode('./script.js', { nodePath: node.path })
    )
});

['12.22.9', '14.18.3', '16.13.2'].map(
    async nodeVersion => runTest(await getNode(nodeVersion))
)

API

test(name, testFunction, timeout?)

name: string

testFunction: () => void

timeout: number

Return value: Promise<void>

Create and run a test.

describe(description, testGroupFunction)

description: string

testGroupFunction: ({ test, describe, runTestSuite }) => void

Return value: Promise<void>

Create a group of tests.

testSuite(testSuiteFunction, ...testSuiteArguments)

testSuiteFunction: ({ test, describe, runTestSuite }) => any

testSuiteArguments: any[]

Return value: (...testSuiteArguments) => Promise<ReturnType<testSuiteFunction>>

Create a test suite.

FAQ

What does Manten mean?

Manten (まんてん, 満点) means "maximum points" or 100% in Japanese.

What's the logo?

It's a Hanamaru symbol:

The hanamaru (はなまる, 花丸) is a variant of the O mark used in Japan, written as 💮︎. It is typically drawn as a spiral surrounded by rounded flower petals, suggesting a flower. It is frequently used in praising or complimenting children, and the motif often appears in children's characters and logos.

The hanamaru is frequently written on tests if a student has achieved full marks or an otherwise outstanding result. It is sometimes used in place of an O mark in grading written response problems if a student's answer is especially good. Some teachers will add more rotations to the spiral the better the answer is. It is also used as a symbol for good weather.

https://en.wikipedia.org/wiki/O_mark

Why is there no test runner?

Currently, Manten does not come with a test runner because the tradeoffs are not worh it.

The primary benefit of the test runner is that it can detect and run all test files, and maybe watch for file changes to re-run tests.

The drawbacks are:

  • Re-invented Node.js binary API. In today's Node.js ecosystem, it's common to see a build step for TypeScript, Babel, etc. By creating a new binary to run JavaScript, we have to re-invent APIs to allow for things like transformations.
  • Test files are implicitly declared. Instead of explicitly specifying test files, the test runner traverses the project to find tests that match a pattern. This search adds an overhead and can incorrectly match files that are not tests (eg. complex test fixtures in projects that are related to testing).
  • Tests in Manten are async first and can run concurrently. While this might be fine in some cases, it can also be too much and may require guidance in how many tests can run in parallel.

Why no beforeAll/beforeEach?

beforeAll/beforeEach hooks usually deal with managing shared environmental variables.

Since Manten puts async flows first, this paradigm doesn't work well when tests are running concurrently.

By creating a new context for each test, a more functional approach can be taken where shared logic is better abstracted and organized.

Code difference

Before

There can be a lot of code between the beforeEach and the actual tests that makes it hard to follow the flow in logic.

beforeAll(async () => {
    doSomethingBeforeAll()
})

beforeEach(() => {
    doSomethingBeforeEach()
})

// There can be a lot of code in between, making
// it hard to see there's logic before each test

test('My test', () => {
    // ...
})

After

Less magic, more explicit code!

await doSomethingBeforeAll()

test('My test', async () => {
    await doSomethingBeforeEach()

    // ...
})

Related

Easily create test fixtures at a temporary file-system path.

Sponsors

Premium sponsor banner Premium sponsor banner