Not another test runner! (natr) But it is, and it is a highly opinionated one that is used for own projects. In case you want to use it as well, you are very welcome to do and give feedback 😎
Important note: This runner is an MVP. Therefore, it is not stable, features missing and not every edge case tested, overall alpha software. Use at own risk. If you want to help get natr on track please read the contribution guide.
Click to show
The goal of natr is a reduced API by only using deep equal to check values for tests. This got inspired by RITEWay and uses the same API. Another goal was readability. Every test case should state a clear goal without over complicating things. By implement TAP it is also possible to already use a wide variety of tools together with natr for test coverage, color output and formatting. Speed is another factor. Tests should be executed often and therefore natr aims to be as fast as possible.
# NPM
npm install @krieselreihe/natr --save-dev
# Yarn
yarn add @krieselreihe/natr --dev
# PNPM
pnpm install @krieselreihe/natr --save-dev
Unit tests are explicit, so there are no global variables injected. Import describe
from the test runner to create a test suite:
import { describe } from "@krieselreihe/natr";
describe("my test suite", (assert) => {
assert({
given: "a basic arithmetic expression",
should: "calculate 2",
actual: 1 + 1,
expected: 2,
});
});
Now you can either execute it with natr by running the following command to execute all your test files:
natr "src/__tests__/*.test.js"
Or you can also use node to execute a single file. Every natr test suite is a standalone executable Javascript file:
node "src/__tests__/my.test.js"
The execute
helper gives you the chance to wrap an executions of multiple expressions to evaluate results. One example could be to retrieve the evaluated result of a promise:
import { describe, execute } from "@krieselreihe/natr";
describe("my test suite", async (assert) => {
assert({
given: "a promise",
should: "resolve",
actual: await execute(() => Promise.resolve(23)),
expected: 23,
});
});
To test if a function throws an error you can use the execute
helper and pass a function to wrap your execution.
import { describe, execute } from "@krieselreihe/natr";
describe("my test suite", (assert) => {
assert({
given: "an expression that will throw",
should: "throw an error",
actual: execute(() => throw new Error("Doh!")),
expected: new Error("Doh!"),
});
});
To render react component you can just use react-test-renderer and its API.
import { describe } from "@krieselreihe/natr";
import render from "react-test-renderer";
const MyComponent = () => (
<div>
<SubComponent foo="bar" />
</div>
);
const SubComponent = () => <p className="sub">Sub</p>;
describe("my test suite", (assert) => {
assert({
given: "a react component",
should: "be of a specific type",
actual: render.create(<MyComponent />).findByType(SubComponent).type,
expected: "SubComponent",
});
assert({
given: "a react component",
should: "have a specific prop",
actual: render.create(<MyComponent />).findByType(SubComponent).props.foo,
expected: "bar",
});
});
Snapshot testing is included and can be used to test React component as JSON tree or regular JavaScript objects as well. The "expected" helper function toMatchSnapshot
is exposed, that will create a snapshot on first run and match against on further.
import { describe, toMatchSnapshot } from "@krieselreihe/natr";
import render from "react-test-renderer";
const MyComponent = () => (
<div>
<SubComponent foo="bar" />
</div>
);
const SubComponent = () => <p className="sub">Sub</p>;
describe("my test suite", (assert) => {
assert({
given: "a react component",
should: "match snapshot",
actual: render.create(<MyComponent />).toJSON(),
expected: toMatchSnapshot(),
});
assert({
given: "JavaScript object",
should: "match snapshot",
actual: { foo: 42 },
expected: toMatchSnapshot(),
});
});
Snapshots will be saved next to the test file in a folder called __snapshots__
as JSON files. To regenerate snapshots you can either delete the snapshot file or use the --updateSnapshot / -u
flag provided by the runner CLI:
natr -u
After installing natr you can use the CLI to execute multiple test files at once. To use glob patterns provided by natr the pattern should be in quotes:
natr "src/**/__tests__/*.(js|jsx)"
It uses fast-glob, you can find documentation about the supported patterns at the fast-glob pattern syntax documentation. If you do not want to use fast-glob for "pattern matching" you can write patterns without quotes to let your shell handle the file matching:
natr src/__tests__/*.js
Note: It is always possible to just use node to execute your tests. There are no magic global variables defined.
# Execute a single test case with node
node src/__tests__/my.test.js
To preload modules and files you can use the --require
(-r
) flag for the CLI. It can also be used multiple times if needed. For example, you can require Babel to enable certain transformations like supporting JSX or new ECMAScript features (for Babel you need also a .babelrc
configuration file to make it work).
# Register babel
natr "src/__tests__/*.test.(js|jsx)" -r @babel/register
# Preload a file
natr "src/__tests__/*.test.(js|jsx)" -r ./tests/setup.js
To update your generated snapshot tests you can add the --updateSnapshot
(-u
) flag to the CLI.
natr -u "src/__tests__/*.test.(js|jsx)"
To generate code coverage reports you can combine istanbul with tap-nyc.
nyc --reporter=text natr "src/__tests__/*.test.js" | tap-nyc
You can also integrate this with coveralls by using node-coveralls and the text-lcov
reporter of istanbul:
nyc --reporter=text-lcov natr "src/__tests__/*.test.js" | coveralls
Through the implementation of TAP you can use a variety of formatter that already exist. As long as they take TAP formatted text as input you can use it. Here is a list of some of them:
- tap-bail
- tap-diff
- tap-dot
- tap-html
- tap-json
- tap-markdown
- tap-nirvana
- tap-notify
- tap-nyan
- tap-spec
- tap-xunit
Probably many, many more. To use any of them install it and pipe the natr output to the respective formatter:
natr "src/__tests__/*.test.js" | tap-nirvana
Described as TypeScript the API would like the following:
interface Assert<TestType = any> {
given: string;
should: string;
actual: TestType;
expected: TestType;
}
type UnitTest = (assert: Assert) => void;
type TestSuite = (test: UnitTest) => Promise<void> | void;
type Describe = (unit: string, fn: TestSuite) => void;
type Execute<ReturnType = any> = (
callback: () => ReturnType,
) => Promise<ReturnType> | ReturnType;
Describes a test suite:
describe(string, TestSuite);
Gets passed by describe
s test suite function and describes a single unit test with an object:
describe(string, [async] assert => {
assert({
given: string,
should: string,
actual: any,
expected: any
});
});
Function to execute code to either a returned value or a thrown error.
assert({
given: string,
should: string,
actual: [await] execute(() => any),
expected: any
});
This runner was highly inspired by RITEWay on how to write tests with focus on simplicity (e.g. only use deep equal, enforce reduced API), and node-tap on how to log test reports based on the TAP (Test Anything Protocol). Also, Jest for snapshot testing and tape on actual creating a runner that had to hold as base for natr.