Lightweight testing library for Node.js
- Minimal API:
test
,describe
,testSuite
- Async first design
- Flow control via
async
/await
- Strongly typed
- Tiny!
2.3 kB
npm i -D manten
- 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 thedescribe
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
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)
})
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.
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 }) => {
// ...
})
})
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.
Run asynchronous tests sequentially by await
ing 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()
})
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()
})
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)
All descendant tests in a describe()
are collected so await
ing on the describe()
will wait for all async tests inside to complete, even if they are not individually await
ed.
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', () => {
// ...
})
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)
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'))
})
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')
})
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')
})
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', () => {
// ...
})
})
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))
)
name: string
testFunction: () => void
timeout: number
Return value: Promise<void>
Create and run a test.
description: string
testGroupFunction: ({ test, describe, runTestSuite }) => void
Return value: Promise<void>
Create a group of tests.
testSuiteFunction: ({ test, describe, runTestSuite }) => any
testSuiteArguments: any[]
Return value: (...testSuiteArguments) => Promise<ReturnType<testSuiteFunction>>
Create a test suite.
Manten (まんてん, 満点) means "maximum points" or 100% in Japanese.
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
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.
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()
// ...
})
Easily create test fixtures at a temporary file-system path.