Lightning-fast unit testing for native ECMAScript modules.
- Run unit tests in real web browsers as pure ECMAScript modules, without any bundling or transpilation steps.
- Easy debugging using any standard JavaScript debugger.
- Each test file is run in an isolated iframe, with a clean window and DOM document to work with. Test files can't normally interfere with each others' mocks.
- Quickly isolate and rerun any failing test by itself, by clicking "Isolate" in the debug browser window.
npm add --save-dev esm-unit
ESM Unit requires ECMAScript module code capable of running in current web browsers without any transpilation step. If your code uses nonstandard tools like JSX, or uses import
to load anything other than native ES modules from local relative file paths, it will need to be converted before running this.
One option is to use a dev server to translate JSX to ESM code on the fly, and replacing nonstandard import
s with their original API versions (such as require()
for AMD packages). You can provide a custom dev server using the --http-server
command line option.
Node 18 LTS or later is required to use ESM Unit. Node 20 LTS is required for development.
In tests/suite.test.js
:
import {
describe,
test,
assert,
before,
beforeEach,
after,
afterEach
} from "esm-unit/test-api.js";
import { wait } from "./util/async.js";
describe("my tests", () => {
// Describes a test suite
before(() => {
// Optional setup run before the first test in this suite
});
after(() => {
// Optional teardown run after the last test in this suite
});
beforeEach(() => {
// Optional setup run before every test in this suite
});
afterEach(() => {
// Optional teardown run before every test in this suite
});
describe("these should pass", () => {
test("synchronous test", () => {
assert.equal(1, 1);
});
test("asynchronous test", async () => {
await wait(10);
assert.equal(1, 1);
});
});
describe("testing failures", () => {
test("synchronous failure", () => {
assert.fail("This should fail");
});
test("asynchronous failure", async () => {
await wait(10);
assert.equal(1, "1", "Equality checks are strict");
});
});
});
Any test that returns a promise is treated as an asynchronous test.
In headless Chrome from the command line:
First, ensure chromedriver
is installed and updated. You can do this by running npm install -g chromedriver
, and ensure the path returned by npm bin -g
is added to your PATH
variable in .bash_profile
for bash or .zshrc
for zsh. Then from the root of your project:
esm-unit --suite=tests/suite.test.js --headless
To specify any other browser, provide the browser name via --browser
:
esm-unit --suite=tests/suite.test.js --browser=firefox
To test multiple browsers in parallel and combine their coverage stats into a single report, this can be set multiple times:
esm-unit --suite=tests/suite.test.js --browser=chrome --browser=safari --browser=firefox
Note: Browser logs are only printed from Chrome, and headless mode is only supported in Chrome and Firefox. Using other browsers may require installing their driver executable, such as geckodriver
.
To open the test in debug mode in your default web browser:
esm-unit --debug --suite=tests/suite.test.js
If a test fails, you can click "Isolate" in the browser to rerun and debug only that test.
Save a config file in esm-unit.json
in the root of your project:
{
"importMap": {
"imports": {
"esm-unit/": "/node_modules/esm-unit/"
}
},
"testFiles": [
"src/**/*.test.js"
]
}
Or in esm-unit.js
to generate it with code:
const { getLocale } = require("./utils.js");
module.exports = {
"importMap": {
"imports": {
"esm-unit/": "/node_modules/esm-unit/",
},
},
"testFiles": [
"src/**/*.test.js",
],
"appendParam": filePath => `?locale=${getLocale(filePath)}`,
};
testFiles
can be file paths or glob patterns. If using glob patterns, you can also provide "excludeTestFiles"
with an array of file paths or patterns to avoid.
Then run esm-unit (with or without --debug
):
esm-unit --headless
Add --watch
to scan your project's directory for any changes and automatically re-run
your tests. This can be configured by setting the root directory to "watchDirectory"
in your esm-unit.json file:
{
"watchDirectory": "src",
"testFiles": [
"src/**/*.test.js"
]
}
The --watch
and --debug
options can't be used at the same time. With --debug
on, refresh the web browser
to re-run tests.
These options are also available in your esm-unit.json
or esm-unit.js
file:
-
module
(boolean): Whether to load test files as native ECMAScript modules. Defaults to true. -
testFiles
(array of glob patterns): A list of paths to scan for test files. -
watchDirectory
(path): Root directory to watch for changes when running with--watch
-
includeScripts
(array of paths): a list of JavaScript files to load as ordinary scripts before each test -
includeModules
(array of paths): a list of JavaScript files to load as native ECMAScript modules before each test -
appendQuery
(string|function): An optional query parameter, like?env=test
, to append to each JavaScript file path. Useful with custom dev servers like es6-module-server. If providing a function, it will take the test file path as an argument. -
coverage
(object): Code coverage configuration. See below for details. -
failOnErrorLogs
(boolean) Tests will be marked as failed if any errors appear in the browser console, such as uncaught tracebacks and console.error messages. (Chrome and MS Edge only). -
importMap
(object): An import map following the WHATWG specification. The object set here will be included in a<script type="importmap">
in test suites. When set, unit tests will only run in web browsers with native support for import maps.
esm-unit supports integrated per-file code coverage enforcement.
Code coverage reports are enabled by default when running without --debug
, but can be disabled with --no-coverage
.
To enable coverage threshold enforcement, add a coverage
configuration block to your esm-unit.json
config file:
"coverage": [
{
"includeFiles": [
"src/**/*.js"
],
"excludeFiles": [
"src/**/*.test.js",
],
"threshold": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
},
]
Add additional objects to the coverage
array to override settings for individual files. Each file will use the thresholds of the last matching object.
Any assertion library is supported, as long as it can be loaded in web browsers as an ECMAScript module.
ESM Unit includes the following built-in assertions, based on the classic JSUnit API. [description]
is an optional but recommended string message to be printed on failure.
Function | Asserts that... |
---|---|
assert(value, [description]) |
value is truthy |
assert.true(value, [description]) |
value is strictly true
|
assert.false(value, [description]) |
value is strictly false
|
assert.equal(expected, actual, [description]) |
actual is strictly equal to expected
|
assert.notEqual(expected, actual, [description]) |
actual is not strictly equal to expected
|
assert.stringMatches(regExp, actual, [description]) |
actual matches the regular expression regExp
|
assert.deepEqual(expected, actual, [description]) |
actual deeply matches the object provided in expected
|
assert.instanceOf(child, constructor, [description]) |
child is an instance of constructor
|
assert.throws(func, [errorText], [description]) |
func throws an error. If errorText is provided, that text must be contained within the error description. |
await assert.rejects(func or promise, [errorText], [description]) |
promise rejects, or func returns a promise that rejects. If errorText is provided, that text must be contained within the error message. |
assert.fail([description]) |
No assertion; always fails |
import {describe, test, assert} from "../node_modules/esm-unit/test-api.js";
describe("math", () => {
test("addition", () => {
assert.equal(4, 2 + 2, "Math should work");
});
});
Sinon is recommended for stubbing and mocking, and performing assertions against those mocks.
npm install --save-dev sinon
As it's a native ECMAScript module, you can add it to your import map in esm-unit.json
for easier imports:
"importMap": {
"imports": {
"esm-unit/": "/node_modules/esm-unit/",
"sinon": "/node_modules/sinon/pkg/sinon-esm.js"
}
},
"testFiles": [
"src/**/*.test.js"
]
Sinon's own assertion API can be used to validate mocks and spies.
import {describe, test, beforeEach} from "esm-unit/test-api.js";
import sinon from "sinon";
describe("sinon's spies", () => {
const sinonSandbox = sinon.createSandbox();
afterEach(() => {
sinonSandbox.restore();
});
test("console.log()", () => {
sinonSandbox.spy(console, "log");
console.log("Hello, World");
sinon.assert.calledWith(console.log, "Hello, World");
});
});
Feel free to open an issue or a pull request!
Make sure to read our code of conduct.
We actively welcome pull requests. Learn how to contribute.