@magic/test

Writing Tests

Single Test

A test can be a literal value, function, or promise:

export default { fn: true, expect: true, info: 'expect true to be true' }// expect: true is the default and can be omittedexport default { fn: true, info: 'expect true to be true' }// if fn is a function, expect is the returned value of the functionexport default { fn: () => false, expect: false, info: 'expect true to be true' }// if expect is a function, the return value of the test gets passed to itexport default { fn: false, expect: t => t === false, info: 'expect true to be true' }// if fn is a promise, the resolved value will be returnedexport default { fn: new Promise(r => r(true)), expect: true, info: 'promise resolves to true' }// if expect is a promise, it will resolve before being compared to the fn return valueexport default { fn: true, expect: new Promise(r => r(true)), info: 'expect is a promise' }// callback functions can be tested easily too:import { promise } from '@magic/test'const fnWithCallback = (err, arg, cb) => cb(err, arg)export default { fn: promise(fnWithCallback(null, 'arg', (e, a) => a)), expect: 'arg'

Multiple Tests

Multiple tests can be created by exporting an array or object of single test objects.

export default [  { fn: () => true, expect: true, info: 'expect true to be true' },  { fn: () => false, expect: false, info: 'expect false to be false' },]

Or exporting an object with named test arrays:

export default {  multipleTests: [    { fn: () => true, expect: true, info: 'expect true to be true' },    { fn: () => false, expect: false, info: 'expect false to be false' },  ]}

Running Tests Multiple Times

Use the `runs` property to run a test multiple times:

import { is } from '@magic/test'export default [  {    fn: Math.random(),    expect: is.number,    runs: 5,    info: 'runs the test 5 times and expects all returns to be numbers',  },]

Testing Types

Types can be compared using @magic/types

@magic/types is a richly featured and thoroughly tested type library without dependencies. It is exported from this library for convenience.

import { is } from '@magic/test'export default [  { fn: () => 'string',    expect: is.string,    info: 'test if a function returns a string'  },  {    fn: () => 'string',    expect: is.length.equal(6),    info: 'test length of returned value'  },  // Testing for deep equality. simple.  {    fn: () => [1, 2, 3],    expect: is.deep.equal([1, 2, 3]),    info: 'deep compare arrays/objects for equality',  },  {    fn: () => ({ key: 1 }),    expect: is.deep.different({ value: 1 }),    info: 'deep compare arrays/objects for difference',  },]

Caveat

If you want to test if a function is a function, you need to wrap the function in a function. This is because functions passed to fn get executed automatically.

import { is } from '@magic/test'const fnToTest = () => {}export default [  {    fn: () => fnToTest,    expect: is.function,    info: 'function is a function',  },]

TypeScript Support

@magic/test supports TypeScript test files. You can write tests in .ts files and they will be executed directly without transpilation.

import type { Test } from '@magic/test'export default [  { fn: () => true, expect: true, info: 'TypeScript test works!' }] satisfies Test[]

This requires Node.js 22.18.0 or later.

Promises

import { promise, is } from '@magic/test'export default [  // kinda clumsy, but works. until you try handling errors.  {    fn: new Promise(cb => setTimeout(() => cb(true), 2000)),    expect: true,    info: 'handle promises',  },  // better!  {    fn: promise(cb => setTimeout(() => cb(null, true), 200)),    expect: true,    info: 'handle promises in a nicer way',  },  {    fn: promise(cb => setTimeout(() => cb(new Error('error')), 200)),    expect: is.error,    info: 'handle promise errors in a nice way',  },]

Callback Functions

import { promise, is } from '@magic/test'const fnWithCallback = (err, arg, cb) => cb(err, arg)export default [  {    fn: promise(cb => fnWithCallback(null, true, cb)),    expect: true,    info: 'handle callback functions as promises',  },  {    fn: promise(cb => fnWithCallback(new Error('oops'), true, cb)),    expect: is.error,    info: 'handle callback function error as promise',  },]

Hooks

Individual Test Hooks

Run functions before and/or after individual tests:

const after = () => {  global.testing = 'Test has finished, cleanup.'}const before = () => {  global.testing = false  // if a function gets returned,  // this function will be executed once the test finishes.  return after}export default [  {    fn: () => { global.testing = 'changed in test' },    before,    after,    expect: () => global.testing === 'changed in test',  },]

Suite Hooks

Run functions before and/or after a suite of tests:

const afterAll = () => {  global.testing = undefined}const beforeAll = () => {  global.testing = false  // if a function gets returned,  // this function will be executed once the test suite finishes.  return afterAll}export default {  beforeAll,  // this is optional if beforeAll returns a function  afterAll,  tests: [    {      fn: () => { global.testing = 'changed in test' },      expect: () => global.testing === 'changed in test',    },  ],}

Note: Suites that use beforeAll, afterAll, beforeEach or afterEach will run in a worker to make sure globals are not polluted for other suites.

File-based Hooks

You can also create test/beforeAll.js and test/afterAll.js files that run before/after all tests.

**Note:** These files must be placed at the **root** `test/` directory (not in subdirectories).

// test/beforeAll.jsexport default () => {  global.setup = true  // optionally return a cleanup function  return () => {    global.setup = false  }}
// test/afterAll.jsexport default () => {  // cleanup after all tests}

beforeEach and afterEach

Define beforeEach and afterEach hooks that run before/after each individual test:

const beforeEach = () => {  // Runs before each test in this suite  global.testState = { initialized: true }}const afterEach = () => {  // Runs after each test  global.testState = null}export default {  beforeEach,  afterEach,  tests: [    { fn: () => global.testState.initialized, expect: true },    { fn: () => true, expect: true },  ],}

Magic Modules

@magic-modules assume all HTML tags to be globally defined. To create those globals for your test and check if a @magic-module returns the correct markup, just use one of those tags in your tests.

const expect = [  'i',  [    { class: 'testing' },    'testing',  ],]const props = { class: 'testing' }export default [  {    fn: () => i(props, 'testing'),    expect,    info: 'magic/test can now test html',  },]

Test Suites

Expectations for optimal test messages:

  • src and test directories have the same structure and files
  • tests one src file per test file
  • tests one function per suite
  • tests one feature per test

Filesystem Based Naming

The following directory structure:

./test/  ./suite1.js  ./suite2.js

yields the same result as exporting the following from ./test/index.js:

import suite1 from './suite1'import suite2 from './suite2'export default {  suite1,  suite2,}

Data Driven Naming

Export test structure directly from index.js:

export default {  suite1: [    { fn: () => true, expect: true },  ],  suite2: [    { fn: () => false, expect: false },  ],}

Important File Mappings

If test/index.js exists, no other files will be loaded. If test/lib/index.js exists, no other files from that subdirectory will be loaded. Instead, the exports of those index.js will be expected to be tests.