Skip to content
⚠️WIP: Not another test runner! But it is, and it is a highly opinionated one.
JavaScript TypeScript
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
scripts
src
.babelrc
.czrc
.editorconfig
.eslintrc.js feat: implement first draft for snapshot testing Jun 23, 2019
.gitignore
.npmrc
.nvmrc
.prettierignore
.travis.yml
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
package.json
pnpm-lock.yaml
renovate.json
tsconfig.json

README.md

natr

Build Status Coverage Status Commitizen friendly code style: prettier

Not another test runner! (natr) But it is, and it is a highly opinionated one that is used for own projects. But if you want to use it as well, you are very welcome to do and give feedback 😎

Important note: This runner is a MVP. Therefore 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.

Contents

Principles

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. Besides that, speed is another factor. Tests should be execute often and therefore natr aims to be as fast as possible.

Installation

# NPM
npm install @krieselreihe/natr --save-dev

# Yarn
yarn add @krieselreihe/natr --dev

# PNPM
pnpm install @krieselreihe/natr --save-dev

Usage

Basic test

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", async 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"

Encapsulate execution

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
  });
});

Note: The execute function always should be awaited regardless of the inner expressions.

Exceptions

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", async assert => {
  assert({
    given: "an expression that will throw",
    should: "throw an error",
    actual: await execute(() => throw new Error("Doh!")),
    expected: new Error("Doh!")
  });
});

Sub checks

Inside executions you can perform assert checks as well that will throw if values are not deeply equal. Therefore the execute function passes a check function to the callback.

import { describe, execute } from "@krieselreihe/natr";

describe("my test suite", async assert => {
  assert({
    given: "user object",
    should: "have the correct user id and structure",
    actual: await execute(check => {
      const user = { id: 1, name: "Helga" };

      check(user, { id: 1, name: "Max" });

      return user.id;
    }),
    expected: 1
  });
});

This would throw and result in a console output like the following:

not ok 1 - Given user object: should have the correct user id and structure
  ---
  expected: |-
    1
  actual: |-
    {
      Error: check() in execute() didn't match:
      { id: 1, name: 'Helga' }
      with:
      { id: 1, name: 'Max' }
      at check ... more stack trace ...
    }

React components

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", async 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

Snapshot testing is included and can be used to test React component as JSON tree or regular JavaScript objects as well. Therefore 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", async 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

CLI

Basic usage

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 and 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

Preload files and modules

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

Update snapshot

To update your generated snapshot tests you can add the --updateSnapshot (-u) flag to the CLI.

natr -u "src/__tests__/*.test.(js|jsx)"

Coverage reports

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

Format output

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:

And 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

API

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>;

type Describe = (unit: string, fn: TestSuite) => void;

type Check = (a: any, b: any) => void;

type Execute<ReturnType = any> = (
  callback: (check: Check) => ReturnType
) => Promise<ReturnType | Error>;

describe

Describes a test suite:

describe(string, TestSuite);

assert

Gets passed by describes 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
  });
});

execute

Function to execute code to either a returned value or a thrown error.

assert({
  given: string,
  should: string,
  actual: await execute(() => any),
  expected: any
});

check

The execute function passes an "check" method that will throw an error if the two passed values are not deep equal.

await execute(check => {
  check(any, any);
})

Disclaimer

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.

You can’t perform that action at this time.