@magic/test
@magic/test
simple tests with lots of utility. ecmascript modules only. runs ecmascript module syntax tests without transpilation. unbelievably fast.
getting started
be in a nodejs project.
install
npm i --save-dev --save-exact @magic/testCreate a test
// create test/functionName.jsimport yourTest from '../path/to/your/file.js'export default [{ fn: () => true, expect: true, info: 'true is true' },// note that the yourTest function will be called automagically{ fn: yourTest, expect: true, info: 'hope this will work ;)'}]
npm scripts
edit package.json
{"scripts": {"test": "t -p", // quick test, only failing tests log"coverage": "t", // get full test output and coverage reports}}
repeated for easy copy pasting (without comments and trailing commas)
"scripts": {"test": "t -p","coverage": "t"}
repeated for easy copy pasting (without comments and trailing commas)
"scripts": {"test": "t -p","coverage": "t"}
quick tests
without coverage
// run the tests:npm test// example output:// (failing tests will print, passing tests are silent)// ### Testing package: @magic/test// Ran 2 tests. Passed 2/2 100%
coverage
@magic/test will automagically generate coverage reports if it is not called with the -p flag.
Example output:
### Testing package: @magic/testPassed 2/2 100%
Faster output from a bigger project:
### Testing package: @artificialmuseum/engineRan 90307 tests in 274.5ms. Passed 90307/90307 100%Ran 90307 tests in 265.5ms. Passed 90307/90307 100%Ran 90307 tests in 268.1ms. Passed 90307/90307 100%
data/fs driven test suite creation:
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
expectations for optimal test messages:
- src and test directories have the same directory structure and filenames
- tests one src file per test file
- tests one function per test suite
- tests one feature per test unit
Filesystem based naming
the following directory structure:
./test/./suite1.js./suite2.js
yields the same result as exporting the following from ./test/index.js
Data driven naming
import suite1 from './suite1'import suite2 from './suite2'export default {suite1,suite2,}
Important - File mappings
if test/index.js exists, no other files will be loaded. if test/index.js exists, no other files from that directory will be loaded, if test/lib/index.js, no other files from that subdirectory will be loaded. instead the exports of those index.js will be expected to be tests
single test
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 get 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: 'expect true to be true' }// if expects 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 true to be true' }// 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" }
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 = () => {}// both the tests will work as expectedexport default [{fn: () => fnToTest,expect: is.function,info: 'function is a function',},{fn: is.fn(fnToTest), // returns true// we do not set expect: true, since that is the default// expect: true,info: 'function is a function',},]
// will not work as expected and instead call fnToTestexport 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.
// test/mytest.tsexport default { fn: () => true, expect: true, info: 'TypeScript test works!' }
This requires Node.js 22.18.0 or later.
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' },]}
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: trueinfo: 'handle callback functions as promises',},{fn: promise(cb => fnWithCallback(new Error('oops'), true, cb)),expect: is.error,info: 'handle callback function error as promise',},]
running tests multiple times
Use the runs property to run a test multiple times:
let counter = 0const increment = () => counter++export default [{fn: increment,expect: 1,runs: 5,info: 'runs the test 5 times and expects final value to be 5',},]
hooks
run functions before and/or after individual test
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 finished.return after}export default [{fn: () => { global.testing = 'changed in test' },// if before returns a function, it will execute after the test.before,after,expect: () => global.testing === 'changed in test',},]
suite hooks
run functions before and/or after a suite of tests
const afterAll = () => {// Test has finished, cleanup.'global.testing = undefined}const beforeAll = () => {global.testing = false// if a function gets returned,// this function will be executed once the test suite finished.return afterAll}export default [{fn: () => { global.testing = 'changed in test' },// if beforeAll returns a function, it will execute after the test suite.beforeAll,// this is optional and can be omitted if beforeall returns a function.// in this example, afterAll will trigger twice.afterAll,expect: () => global.testing === 'changed in test',},]
File-based Hooks:
You can also create test/beforeAll.js and test/afterAll.js files that run before/after all tests in a suite.
**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 functionreturn () => {global.setup = false}}
// test/afterAll.jsexport default () => {// cleanup after all tests}
beforeEach and afterEach hooks
Define beforeEach and afterEach hooks in your test objects that run before/after each individual test:
const beforeEach = () => {// Runs before each test in this suiteglobal.testState = { initialized: true }}const afterEach = (testResult) => {// Runs after each test, receives the test resultconsole.log('Test completed:', testResult?.pass)}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 [// note that fn is a wrapped function, we can not call i directly as we could other functions{fn: () => i(props, 'testing'),expect,info: 'magic/test can now test html',},]
Utility Belt
@magic/test exports some utility functions that make working with complex test workflows simpler.
deep
Exported from @magic/deep, deep equality and comparison utilities.
import { deep, is } from '@magic/test'export default [{fn: () => ({ a: 1, b: 2 }),expect: deep.equal({ a: 1, b: 2 }),info: 'deep equals comparison',},{fn: () => ({ a: 1 }),expect: deep.different({ a: 2 }),info: 'deep different comparison',},{fn: () => ({ a: { b: 1 } }),expect: deep.equal({ a: { b: 1 } }),info: 'nested deep equality',},]
Available functions:
- deep.equal(a, b) - deep equality check
- deep.different(a, b) - deep difference check
- deep.contains(container, item) - deep inclusion check
- deep.changes(a, b) - get differences between objects
fs
Exported from @magic/fs, file system utilities.
import { fs } from '@magic/test'export default [{fn: async () => {const content = await fs.readFile('./package.json', 'utf-8')return content.includes('name')},expect: true,info: 'read file content',},]
Common methods:
- fs.readFile(path, encoding) - read file content
- fs.writeFile(path, data) - write file content
- fs.exists(path) - check if file exists
- fs.mkdir(path, options) - create directory
- fs.rmdir(path) - remove directory
- fs.stat(path) - get file stats
- fs.readdir(path) - read directory contents
- Plus async versions in fs.promises
curry
Currying can be used to split the arguments of a function into multiple nested functions. This helps if you have a function with complicated arguments that you just want to quickly shim.
import { curry } from '@magic/test'const compare = (a, b) => a === bconst curried = curry(compare)const shimmed = curried('shimmed_value')export default {fn: shimmed('shimmed_value'),expect: true,info: 'expect will be called with a and b and a will equal b',}
log
Logging utility for test output. Colors supported automatically.
import { log } from '@magic/test'log.debug('Debug info')log.info('Something happened')log.warn('Heads up')log.error('Something went wrong')log.critical('Game over')
Supports template strings and arrays:
log.info('Testing', library, 'at version', version)vals
Exports JavaScript type constants for testing against any value. Useful for fuzzing and property-based testing.
import { vals, is } from '@magic/test'export default [{ fn: () => 'test', expect: is.string, info: 'test if value is a string' },{ fn: () => vals.true, expect: true, info: 'boolean true value' },{ fn: () => vals.email, expect: is.email, info: 'valid email format' },{ fn: () => vals.error, expect: is.error, info: 'error instance' },]
Available Constants:
- Primitives: true, false, number, num, float, int, string, str
- Empty values: nil, emptystr, emptyobject, emptyarray, undef
- Collections: array, object, obj
- Time: date, time
- Errors: error, err
- Colors: rgb, rgba, hex3, hex6, hexa4, hexa8
- Other: func, truthy, falsy, email, regexp
env
Environment detection utilities for conditional test behavior.
Available utilities:
- isNodeProd - checks if NODE_ENV is set to production
- isNodeDev - checks if NODE_ENV is set to development
- isProd - checks if -p flag is passed to the CLI
- isVerbose - checks if -l flag is passed to the CLI
- getErrorLength - returns error length limit from MAGIC_TEST_ERROR_LENGTH env var (0 = unlimited)
import { env, isProd, isTest, isDev } from '@magic/test'export default [{fn: env.isNodeProd,expect: process.env.NODE_ENV === 'production',info: 'checks if NODE_ENV is production',},{fn: env.isNodeDev,expect: process.env.NODE_ENV === 'development',info: 'checks if NODE_ENV is development',},{fn: env.isProd,expect: process.argv.includes('-p'),info: 'checks if -p flag is passed',},{fn: env.isVerbose,expect: process.argv.includes('-l'),info: 'checks if -l flag is passed',},{fn: env.getErrorLength,expect: 70,info: 'get error length limit',},]
Environment Constants
These boolean constants reflect the current NODE_ENV:
import { isProd, isTest, isDev } from '@magic/test'export default [{ fn: isProd, expect: process.env.NODE_ENV === 'production' },{ fn: isTest, expect: process.env.NODE_ENV === 'test' },{ fn: isDev, expect: process.env.NODE_ENV === 'development' },]
promises
Helper function to wrap nodejs callback functions and promises with ease. Handle the try/catch steps internally and return a resolved or rejected promise.
import { promise, is } from '@magic/test'export default [{fn: promise(cb => setTimeOut(() => cb(null, true), 200)),expect: true,info: 'handle promises in a nice way',},{fn: promise(cb => setTimeOut(() => cb(new Error('error'), 200)),expect: is.error,info: 'handle promise errors in a nice way',},]
http
HTTP utility for making requests in tests. Supports both HTTP and HTTPS.
import { http } from '@magic/test'export default [{fn: http.get('https://api.example.com/data'),expect: { success: true },info: 'fetches data from API',},{fn: http.post('https://api.example.com/users', { name: 'John' }),expect: { id: 1, name: 'John' },info: 'creates a new user',},{fn: http.post('http://localhost:3000/data', 'raw string'),expect: 'raw string',info: 'posts raw string data',},]
Error Handling:
import { http, is } from '@magic/test'export default [{fn: http.get('https://invalid-domain-that-does-not-exist.com'),expect: is.error,info: 'rejects on network error',},{fn: http.get('https://api.example.com/nonexistent'),expect: res => res.status === 404,info: 'handles 404 responses',},]
Note: HTTP module automatically handles protocol detection, JSON parsing, and rejectUnauthorized: false
trycatch
allows to test functions without bubbling the errors up into the runtime
import { is, tryCatch } from '@magic/test'const throwing = () => throw new Error('oops')const healthy = () => trueexport default [{fn: tryCatch(throwing()),expect: is.error,info: 'function throws an error',},{fn: tryCatch(healthy()),expect: true,info: 'function does not throw']
error
exports @magic/error which returns errors with optional names.
import { error } from '@magic/test'export default [{fn: tryCatch(error('Message', 'E_NAME')),expect: e => e.name === 'E_NAME' && e.message === 'Message',info: 'Errors have messages and (optional) names.',},]
mock
Mock and spy utilities for function testing.
import { mock, tryCatch } from '@magic/test'export default [{fn: () => {const spy = mock.fn()spy('arg1')return spy.calls.length === 1 && spy.calls[0][0] === 'arg1'},expect: true,info: 'mock.fn tracks call arguments',},{fn: () => {const spy = mock.fn().mockReturnValue('mocked')return spy() === 'mocked'},expect: true,info: 'mock.fn.mockReturnValue sets return value',},{fn: async () => {const spy = mock.fn().mockThrow(new Error('fail'))const caught = await tryCatch(spy)()return caught instanceof Error},expect: true,info: 'mock.fn.mockThrow works with tryCatch',},{fn: () => {const obj = { greet: () => 'hello' }const spy = mock.spy(obj, 'greet', () => 'world')const result = obj.greet()spy.mockRestore()return result === 'world' && obj.greet() === 'hello'},expect: true,info: 'mock.spy replaces and restores methods',},]
mock.fn properties:
- calls - Array of all call arguments
- returns - Array of all return values
- errors - Array of all thrown errors (null for non-throwing calls)
- callCount - Number of times called
mock.fn methods:
- mockReturnValue(value) - Set return value (chainable)
- mockThrow(error) - Set error to throw (chainable)
- getCalls() - Get all call arguments
- getReturns() - Get all return values
- getErrors() - Get all thrown errors
version
The version plugin checks your code according to a spec defined by you. This is designed to warn you on changes to your exports.
Internally, the version function calls @magic/types and all functions exported from it are valid type strings in version specs.
// test/spec.jsimport { version } from '@magic/test'// import your lib as your codebase requires// import * as lib from '../src/index.js'// import lib from '../src/index.jsconst spec = {stringValue: 'string',numberValue: 'number',objectValue: ['obj',{key: 'Willbechecked',},],objectNoChildCheck: ['obj',false,],}export default version(lib, spec)
The spec supports testing parent objects without checking their child properties by using `false` as the second element:
DOM Environment
@magic/test automatically initializes a DOM environment when imported, making browser APIs available in Node.js.
Available globals:
- Core: document, window, self, navigator, location, history
- DOM types: Node, Element, HTMLElement, SVGElement, Document, DocumentFragment
- Events: Event, CustomEvent, MouseEvent, KeyboardEvent, InputEvent, TouchEvent, PointerEvent
- Forms: FormData, File, FileList, Blob
- Networking: URL, URLSearchParams, XMLHttpRequest, fetch, WebSocket
- Storage: Storage, sessionStorage, localStorage
- Observers: MutationObserver, IntersectionObserver, ResizeObserver
- Timers: setTimeout, setInterval, requestAnimationFrame
DOM Utilities:
import { initDOM, getDocument, getWindow } from '@magic/test'// Get the document and window instancesconst doc = getDocument()const win = getWindow()// Manually re-initialize if neededinitDOM()
Canvas/Image Polyfills:
- new Image() - Parses PNG data URLs to extract dimensions
- canvas.getContext("2d") - Returns node-canvas context
- canvas.toDataURL() - Serializes canvas to data URL
svelte
@magic/test includes built-in support for testing Svelte 5 components. It compiles Svelte components, mounts them in a DOM environment, and provides utilities for interacting with and asserting on component behavior.
import { mount, html, tryCatch } from '@magic/test'const component = './path/to/MyComponent.svelte'export default [{component,props: { message: 'Hello' },fn: ({ target }) => html(target).includes('Hello'),expect: true,info: 'renders the message prop',},]
Exported Functions:
- mount(filePath, options) - Mounts a Svelte component
- html(target) - Returns innerHTML
- text(target) - Returns textContent
- component(instance) - Returns component instance
- props(target) - Returns attribute name/value pairs
- click(target, selector?) - Clicks an element
- dblClick(target) - Double clicks
- contextMenu(target) - Right click
- mouseDown(target) - Mouse down
- mouseUp(target) - Mouse up
- mouseMove(target) - Mouse move
- mouseEnter/Leave/Over/Out - Mouse events
- keyDown/Press/Up - Keyboard events
- type(target, text) - Type text into input
- input(target, value) - Input value
- change(target, value) - Change event
- blur/focus - Focus events
- submit(target) - Submit form
- pointer/touch events - Pointer and touch
- copy/cut/paste - Clipboard
- drag events - Drag and drop
- resize(target, w, h) - Resize
- scroll(target, x, y) - Scroll
- animation/transition events - CSS events
- play/pause - Media
- trigger(target, eventType, options) - Custom event
- checked(target) - Checkbox state
Test Properties:
- component - Path to the .svelte file
- props - Props to pass to the component
- fn - Test function receiving { target, component, unmount }
Example: Accessing Component State
import { mount, html } from '@magic/test'const component = './src/lib/svelte/components/Counter.svelte'export default [{component,fn: async ({ target, component: instance }) => {return instance.count},expect: 0,info: 'initial count is 0',},]
Automatic Test Exports
When testing Svelte 5 components, @magic/test automatically exports $state and $derived variables, making them accessible in tests without requiring manual exports.
**Note:** This automatic export feature is specific to **Svelte 5** only. Svelte 4 components do not have this capability.
<!-- Component.svelte --><script>let count = $state(0)let doubled = $derived(count * 2)<!-- No export needed! --></script><button class="inc">+</button><span>{doubled}</span>
Test - works automatically!
import { mount } from '@magic/test'export default [{component: './Component.svelte',fn: async ({ component }) => component.count, // 0expect: 0,info: 'access $state without manual export',},{component: './Component.svelte',fn: async ({ component }) => component.doubled, // 0 (derived)expect: 0,info: 'access $derived without manual export',},]
This works automatically for all $state and $derived runes. No configuration needed!
Example: Testing Error Handling
import { mount, tryCatch } from '@magic/test'const component = './src/lib/svelte/components/MyComponent.svelte'export default [{fn: tryCatch(mount, component, { props: null }),expect: t => t.message === 'Props must be an object, got object',info: 'throws when props is null',},]
SvelteKit Mocks:
Mocks SvelteKit $app modules:
import { browser, dev, prod, createStaticPage } from '@magic/test'export default [{fn: () => browser, // true if in browser environmentexpect: false,info: 'not in browser by default',},{fn: () => dev, // true if in dev modeexpect: process.env.NODE_ENV === 'development',info: 'dev reflects NODE_ENV',},{fn: () => prod, // true if in production modeexpect: false,info: 'not in prod by default',},]
compileSvelte
Compile Svelte component source to a module for testing:
import { compileSvelte } from '@magic/test'export default [{fn: async () => {const source = `<button>Click</button>`const { js, css } = compileSvelte(source, 'button.svelte')return js.code.includes('button') && css.code === ''},expect: true,info: 'compiles Svelte source to module',},]
Native Node.js Test Runner
@magic/test includes a native Node.js test runner that uses the built-in --test flag. This provides better integration with Node.js ecosystem tools and IDEs.
Usage
# Run tests using Node.js native test runnernpm run test:native
Add to your package.json:
{"scripts": {"test": "t -p","test:native": "node --test src/bin/node-test-runner.js"}}
Using in External Libraries
To use the native test runner in your own library that depends on @magic/test:
1. Copy the runner file to your project:
# Copy node-test-runner.js to your projectcp node_modules/@magic/test/src/bin/node-test-runner.js src/
2. Update the paths in the runner if needed (it uses relative paths to find the test directory)
3. Add the script to your package.json:
{"scripts": {"test": "t -p","test:native": "node --test src/bin/node-test-runner.js"}}
Features
The native runner supports all the same features as the custom runner:
- Test file discovery (.js, .mjs, .ts)
- File-based hooks (beforeAll.js, afterAll.js)
- Svelte component testing
- All assertion types
- Global magic modules
Differences from Custom Runner
| Feature | Custom Runner | Native Runner |
|---------|--------------|---------------|
| Test discovery | Custom glob patterns | Node.js --test patterns |
| Output format | Colored CLI output | Node.js test format |
| Hooks | Full support | Full support |
| Coverage | Via c8 | Not available |
Test Isolation
@magic/test supports test isolation to prevent tests from affecting each other. Tests in the same suite can share state, but you can isolate them:
export default [// This test runs in isolation from others{fn: () => {const state = { counter: 0 }state.counter++return state.counter},expect: 1,info: 'isolated test with local state',},]
Global Isolation Mode:
By default, tests in the same file share global state. To enable strict isolation where each test gets a fresh environment:
// This runs each test in isolation with fresh globalsexport const __isolate = trueexport default [{ fn: () => global.test = 1, expect: 1 },{ fn: () => global.test === undefined, expect: true, info: 'fresh global state' },]
Programmatic Detection:
You can programmatically check if a suite requires isolation using the `suiteNeedsIsolation` utility:
import { suiteNeedsIsolation } from '@magic/test'const needsIsolation = suiteNeedsIsolation(tests)
This is useful for custom runners or when building test tooling.
usage
js
// test/index.jsimport { run } from '@magic/test'const tests = {lib: [{ fn: () => true, expect: true, info: 'Expect true to be true' }],}run(tests)
cli
package.json (recommended)
add the magic/test bin scripts to package.json
{"scripts": {"test": "t -p","coverage": "t",},"devDependencies": {"@magic/test": "github:magic/test"}}
then use the npm run scripts
npm testnpm run coverage
Globally (not recommended):
you can install this library globally, but the recommendation is to add the dependency and scripts to the package.json file.
this both explains to everyone that your app has these dependencies as well as keeping your bash free of clutter
npm i -g @magic/test// run tests in production modet -p// run tests and get coverage in verbose modet
CLI Flags
Available command-line flags:
- -p, --production, --prod - Run tests without coverage (faster)
- -l, --verbose, --loud - Show detailed output including passing tests
- -i, --include - Files to include in coverage
- -e, --exclude - Files to exclude from coverage
- --shards, --shard-count - Total number of shards to split tests across
- --shard-id - Shard ID (0-indexed) to run
- --help - Show help text
Note: `--shards` and `--shard-id` must be used together. `--shard-id` is 0-indexed (0 to N-1).
Common Usage:
# Quick test run (no coverage, fails show errors)npm test # or: t -p# Full test with coverage reportnpm run coverage # or: t# Verbose output (shows passing tests)t -l# Test with coverage for specific filest -i "src/**/*.js"# Use glob patterns for include/excludet -i "src/**/*.js" -e "**/*.spec.js"# Run tests with sharding (for parallel CI)t --shards 4 --shard-id 0
Sharding Tests
Run tests in parallel across multiple processes to speed up large test suites.
# Run 4 shards, this is shard 0 (of 0-3)t --shards 4 --shard-id 0# Run shard 1t --shards 4 --shard-id 1# Combine with other flagst -p --shards 4 --shard-id 2
Tests are distributed deterministically using a hash of the test file path, ensuring each test always runs in the same shard.
Add to your package.json for CI/CD:
{"scripts": {"test": "t -p","test:shard:0": "t -p --shards 4 --shard-id 0","test:shard:1": "t -p --shards 4 --shard-id 1","test:shard:2": "t -p --shards 4 --shard-id 2","test:shard:3": "t -p --shards 4 --shard-id 3"}}
Or use a single command to run all shards in parallel:
# Run all 4 shards in parallel and wait for all to completenpm run test:shard:0 & npm run test:shard:1 & npm run test:shard:2 & npm run test:shard:3 & wait
This library tests itself, have a look at the tests Checkout @magic/types and the other magic libraries for more test examples.
Exit Codes
@magic/test returns specific exit codes to indicate test results:
| Exit Code | Meaning |
| --------- | ------- |
| 0 | All tests passed |
| 1 | One or more tests failed |
# Run tests and check exit codenpm testecho "Exit code: $?" # 0 = success, 1 = failure
Performance Tips
Follow these tips to get the most out of @magic/test:
Use the -p flag for development:
# Fast mode - no coverage, only shows failuresnpm test# ort -p
Shard large test suites:
# Split tests across multiple processest --shards 4 --shard-id 0
Run tests in parallel with native runner:
# Native runner uses Node.js built-in test runnernpm run test:native
Minimize async overhead:
# Slower: unnecessary asyncexport default {fn: async () => {return true},expect: true,}# Faster: sync testexport default {fn: () => true,expect: true,}
Use local state instead of globals:
# Slower: global state requires isolationexport const __isolate = true# Faster: local state is naturally isolatedexport default [{fn: () => {const counter = 0return ++counter},expect: 1,},]
Batch related tests:
# Faster: single suite with multiple testsexport default [{ fn: () => add(1, 2), expect: 3 },{ fn: () => add(0, 0), expect: 0 },{ fn: () => add(-1, 1), expect: 0 },]
Verbose Output
The -l (or --verbose, --loud) flag enables detailed output:
# Shows all tests including passing onest -l
What verbose mode shows:
- All test results (not just failures)
- Individual test execution time
- Full test names with suite hierarchy
- Detailed error messages with stack traces
Default mode (without -l):
- Only shows failing tests
- Shows summary only for passing suites
- Faster output for large test suites
Example output without -l:
### Testing package: my-lib/addition.js => Pass: 3/3 100%/multiplication.js => Pass: 4/4 100%Ran 7 tests in 12ms. Passed 7/7 100%
Example output with -l:
### Testing package: my-lib▶ addition✔ adds two positive numbers (1.2ms)✔ handles zero correctly (0.8ms)✔ handles negative numbers (0.9ms)▶ multiplication✔ multiplies by zero (0.7ms)✔ multiplies by one (0.6ms)✔ multiplies two positives (0.8ms)✔ handles negative numbers (0.9ms)Ran 7 tests in 12ms. Passed 7/7 100%
Common Pitfalls
Avoid these common mistakes when writing tests:
1. Forgetting to return in async tests:
# Wrong: promise resolves before test checks resultexport default {fn: async () => {const result = await someAsyncFunction()# missing return!},expect: true,}# Correct:export default {fn: async () => {return await someAsyncFunction()},expect: true,}
2. Not wrapping callback functions:
# Wrong: function gets called immediatelyexport default {fn: doSomething(), # executes immediately!expect: true,}# Correct: wrap in function to defer executionexport default {fn: () => doSomething(),expect: true,}
3. Mutating shared state between tests:
# Wrong: counter persists between testslet counter = 0export default [{ fn: () => ++counter, expect: 1 },{ fn: () => ++counter, expect: 2 }, # fails! counter is now 1]# Correct: use local state or reset in beforeEachlet counter = 0const beforeEach = () => { counter = 0 }export default {beforeEach,tests: [{ fn: () => ++counter, expect: 1 },{ fn: () => ++counter, expect: 1 }, # passes - reset before each],}
4. Using the wrong equality check:
# Wrong: checks reference equalityexport default {fn: () => [1, 2, 3],expect: [1, 2, 3], # fails! different arrays}# Correct: use @magic/types for deep comparisonimport { is } from '@magic/test'export default {fn: () => [1, 2, 3],expect: is.deep.equal([1, 2, 3]),}
5. Not awaiting async operations:
# Wrong: test finishes before promise resolvesexport default {fn: () => {setTimeout(() => {# This never gets checked!}, 100)},expect: true,}# Correct: return the promiseexport default {fn: () => new Promise(resolve => {setTimeout(() => resolve(true), 100)}),expect: true,}# Or use the promise helper:import { promise } from '@magic/test'export default {fn: promise(cb => setTimeout(() => cb(null, true), 100)),expect: true,}
6. Incorrect hook usage:
# Wrong: before/after hooks on individual tests, not suitesexport default [{fn: () => true,beforeAll: () => {}, # wrong! beforeAll is for suitesafterAll: () => {},expect: true,},]# Correct: hooks at suite levelconst beforeAll = () => {}const afterAll = () => {}export default {beforeAll,afterAll,tests: [{ fn: () => true, expect: true },],}
Error Codes
@magic/test uses error codes to help with debugging and programmatic error handling. You can import these constants from `@magic/test`:
import { ERRORS, errorify } from '@magic/test'Available error codes:
- ERRORS.E_EMPTY_SUITE - Test suite is not exporting any tests
- ERRORS.E_RUN_SUITE_UNKNOWN - Unknown error occurred while running a suite
- ERRORS.E_TEST_NO_FN - Test object is missing the fn property
- ERRORS.E_TEST_EXPECT - Test expectation failed
- ERRORS.E_TEST_BEFORE - Before hook failed
- ERRORS.E_TEST_AFTER - After hook failed
- ERRORS.E_TEST_FN - Test function threw an error
- ERRORS.E_NO_TESTS - No test suites found
- ERRORS.E_IMPORT - Failed to import a test file
- ERRORS.E_MAGIC_TEST - General test execution error
Example usage:
try {// run tests} catch (e) {if (e.code === ERRORS.E_TEST_NO_FN) {console.error('Test is missing fn property:', e.message)}}