npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nole

v3.4.0

Published

Testing Library for Typescript projects

Downloads

918

Readme

Nole

A test runner for TypeScript. A class is a suite. A method is a test.

// test/queue.test.ts
import { Test } from "nole";

export class QueueTest extends Test() {
  queue!: Queue;

  createInstance() {
    this.queue = new Queue();
  }

  push() {
    this.queue.push(10);
  }

  async pipe() {
    await this.queue.pipe(somewhere);
  }
}
$ nole './test/**/*.test.ts'
   (ok)      0.06 ms QueueTest.createInstance()
   (ok)         0 ms QueueTest.push()
   (ok)      1.63 ms QueueTest.pipe()

  3 passed  (2.48 ms)

 discover: 2.68 ms
  resolve: 80.23 ms
    tests: 2.48 ms

No describe, no it, no registration. Export the class and it runs.

Install

npm i -D nole tsx
{
  "scripts": {
    "test": "nole './test/**/*.test.ts'"
  }
}

Quote the glob. Nole expands it, not your shell.

When a test fails

The run keeps going. Failures are collected and shown at the end.

$ nole './test/**/*.test.ts'
   (ok)      0.05 ms CartTest.addsItem()
 (failed)    0.52 ms CartTest.checksTotal()
           AssertionError: 24 == 25
   (ok)      0.01 ms CartTest.pays()

  Failures:

  1) CartTest.checksTotal()
     AssertionError [ERR_ASSERTION]: 24 == 25
         at CartTest.checksTotal (test/cart.test.ts:10:12)

     - expected + actual

     - 25
     + 24

  2 passed, 1 failed  (1.37 ms)

The stack has no nole frames in it. Only your code.

Skip

Start the name with skip.

export class QueueTest extends Test() {
  skip_drain() {}
}
  (skip)             QueueTest.skip_drain() {marked as skipped}

Careful. Any name that starts with skip is skipped, so skipsBadInput() never runs. Call it rejectsBadInput().

Skip the whole class.

export class QueueTest extends Test({ skip: "no need" }) {
  push() {}
}

Skip one test at runtime.

import { Test, skipTest } from "nole";

export class QueueTest extends Test() {
  needsRedis() {
    if (!process.env.REDIS_URL) {
      skipTest("REDIS_URL is not set");
    }
  }
}
 (dskip)             QueueTest.needsRedis() {REDIS_URL is not set}

Skip the rest of the class at runtime. This works in before too.

import { Test, skipClass } from "nole";

export class RemoteTest extends Test() {
  before() {
    if (!process.env.STAGING_URL) {
      skipClass("STAGING_URL is not set");
    }
  }

  ping() {}
  pong() {}
}
 (dskip)             RemoteTest:before() {STAGING_URL is not set}
 (dskip)             RemoteTest.ping() {STAGING_URL is not set}
                      ↳ skip came from :before
 (dskip)             RemoteTest.pong() {STAGING_URL is not set}
                      ↳ skip came from :before

skipTest() inside a hook is an error. You meant skipClass().

Helper methods

Every method is a test. Start the name with _ to keep one for yourself.

export class QueueTest extends Test() {
  async _connect() {
    return new Connection("...");
  }

  async push() {
    const conn = await this._connect();
    conn.push(10);
  }
}

Change the prefix with --spec-skip-prefix or SPEC_SKIP_PREFIX.

Hooks

export class HookTest extends Test() {
  value = 0;

  before() {}
  beforeEach() {}
  afterEach() {}
  after() {}
  cleanUp() {}

  checks() {}
}

Order:

before        once, per class
  beforeEach  before every test
  test
  afterEach   after every test
after         once, per class
cleanUp       once, after everything that depends on this class is done

Want more than one hook of a kind? Start the name with hook. End it with the kind you want. No ending means before.

export class QueueTest extends Test() {
  hookConnect() {}
  hookSeedBeforeEach() {}
  hookCloseAfter() {}

  push() {}
}
   (ok)      0.14 ms QueueTest.hookConnect:before()
   (ok)      0.01 ms QueueTest.hookSeedBeforeEach:beforeEach()
   (ok)      0.03 ms QueueTest.push()
   (ok)      0.01 ms QueueTest.hookCloseAfter:after()

cleanUp always runs. After a failure, after --bail, after ctrl+c.

Dependencies

export class DatabaseTest extends Test() {
  connection = { open: false };

  connect() {
    this.connection.open = true;
  }

  cleanUp() {
    this.connection.open = false;
    console.log("connection closed");
  }
}

export class OrderTest extends Test({
  dependencies: { database: () => DatabaseTest },
}) {
  reads() {
    if (!this.database.connection.open) throw new Error("no connection");
  }
}
   (ok)      0.06 ms DatabaseTest.connect()
   (ok)      0.01 ms OrderTest.reads()
connection closed
   (ok)      0.05 ms DatabaseTest:cleanUp()

DatabaseTest runs first. Its cleanUp runs last, after OrderTest is done.

Did your dependency get skipped?

import { Test, isSkipped, skipClass } from "nole";

export class OrderTest extends Test({
  dependencies: { database: () => DatabaseTest },
}) {
  reads() {
    if (isSkipped(this.database)) {
      skipClass("no database, no orders");
    }
  }
}

Order without dependencies

export class A extends Test() { a() {} }

// B runs before A
export class B extends Test({ before: () => [A] }) { b() {} }

// C runs after A
export class C extends Test({ after: () => [A] }) { c() {} }
   (ok)      0.05 ms B.b()
   (ok)         0 ms A.a()
   (ok)         0 ms C.c()

Inheritance

class Simple extends Test() {
  value = 1;

  check() {}
}

// not exported, so nole ignores it. The children below still run.
export class Complex extends Simple {
  check() {
    super.check();
  }
}

export class MoreComplex extends Simple {
  moreSpecs() {}
}

Parent tests run first, so a child can build on what the parent left behind.

export class GrandparentTest extends Test() {
  variable = 1;

  doesSomething() {
    assert.equal(this.variable, 1);
    this.variable = 2;
  }
}

export class ChildTest extends GrandparentTest {
  doesMoreStuff() {
    assert.equal(this.variable, 2);
  }
}
   (ok)      0.08 ms ChildTest.doesSomething()
   (ok)      0.01 ms ChildTest.doesMoreStuff()
   (ok)         0 ms GrandparentTest.doesSomething()

Both classes are exported, so both run. Each one gets its own instance.

Dynamic tests

import { Test, addTest } from "nole";

// not exported, nole cannot find it
class DynamicTest extends Test() {
  test() {}
}

addTest(() => DynamicTest);

Add one from inside a test.

export class DeepTest extends Test() {
  check() {
    if (something) {
      addTest(() => class Wololo extends Test() {});
    }
  }
}

Reach another class from inside a test with getTest. It returns null if that class has not been registered yet.

import { Test, getTest } from "nole";

export class ReportTest extends Test() {
  reads() {
    const database = getTest(() => DatabaseTest);

    console.log(database?.connection);
  }
}

Timeouts

Default is 5000 ms.

export class SlowTest extends Test({ timeout: 30_000 }) {}
$ nole './test/**/*.test.ts' --timeout 10000

Only know at runtime? Call timeout(). The clock restarts from there.

import { Test, timeout } from "nole";

export class UploadTest extends Test() {
  async uploadsABigFile() {
    timeout(60_000);

    await upload(bigFile);
  }
}

Retries

let tries = 0;

export class NetworkTest extends Test({ retries: 2 }) {
  fetches() {
    if (++tries < 3) throw new Error("connection reset");
  }
}
 (retry)             NetworkTest.fetches() {attempt 1 failed, 2 left}
 (retry)             NetworkTest.fetches() {attempt 2 failed, 1 left}
   (ok)         0 ms NetworkTest.fetches() (flaky, passed on attempt 3)

  1 passed, 1 flaky  (1.37 ms)

A test that only passes on a retry is flaky, not green.

beforeEach and afterEach run again on every attempt.

Same thing for every class:

$ nole './test/**/*.test.ts' --retries 2

Bail

Stop the whole run at the first failure.

$ nole './test/**/*.test.ts' --bail

Stop one class only. Its tests build on each other, so the rest is noise.

export class CartTest extends Test({ bail: true }) {
  addsItem() {}
  checksTotal() {}
  pays() {}
}
   (ok)      0.05 ms CartTest.addsItem()
 (failed)    0.52 ms CartTest.checksTotal()
           AssertionError: 24 == 25
 (block)             CartTest.pays() {an earlier spec of this class failed}
   (ok)      0.02 ms OtherTest.stillRuns()

Other classes keep running. after and cleanUp still run.

Run classes in parallel

$ nole './test/**/*.test.ts' --concurrency 4

Dependencies are respected. Output of each class is printed as one block.

Tests inside one class always run in order. They share an instance.

Some classes cannot share the machine. A port, a directory, an env var.

export class MigrationTest extends Test({ exclusive: true }) {
  async migrates() {
    process.chdir("./tmp/workspace"); // nothing else is running right now
  }
}

Nole waits for the running classes to finish, runs this one alone, then goes back to normal. Free at --concurrency 1.

Run one test

$ nole './test/**/*.test.ts' --grep 'QueueTest.push'
   (ok)      0.05 ms QueueTest.push()

  1 passed, 2 filtered  (0.5 ms)

A /pattern/ is a regular expression.

$ nole './test/**/*.test.ts' --grep '/^Queue.*\.p/'

Classes your match depends on still run in full. They set up the state it needs.

Reporters

$ nole './test/**/*.test.ts' --reporter dot

spec (default), dot, json, tap, junit.

$ nole './test/**/*.test.ts' -r tap
TAP version 13
ok 1 - DatabaseTest.connect
ok 2 - OrderTest.reads
ok 3 - RemoteTest.ping # SKIP STAGING_URL is not set
1..3

Keep the readable output and write a file for CI at the same time.

$ nole './test/**/*.test.ts' --report junit:reports/results.xml --report json:reports/results.json

Config file

Drop a nole.config.ts next to your package.json.

import { defineConfig } from "nole";

export default defineConfig({
  globs: ["./test/**/*.test.ts"],
  timeout: 10_000,
  retries: 1,
  concurrency: 4,
  reporter: "dot",
  reports: [{ name: "junit", file: "./reports/results.xml" }],
});
$ nole

Flags beat the config file. .js, .mjs and .cjs work too.

Watch

$ nole './test/**/*.test.ts' --watch

Every pass runs in a fresh process, so your changes are really reloaded.

Coverage

$ nole './test/**/*.test.ts' --coverage

Runs the same command through the c8 you already have.

CommonJS

Both builds ship. import gets ESM, require gets CommonJS.

import { Test } from "nole"; // ESM
const { Test } = require("nole"); // CommonJS

TypeScript is optional.

// test/queue.test.js
const { Test } = require("nole");

exports.QueueTest = class QueueTest extends Test() {
  check() {
    console.log("It works from CommonJS");
  }
};

Both kinds of file can live in one run.

$ nole './test/**/*.test.{ts,js}'
   (ok)      0.11 ms QueueTest.push()
   (ok)      0.58 ms CommonJSTest.check()

Exit codes

0    everything passed
1    something failed
130  ctrl+c

Options

Usage: nole <glob> ... [options]

Arguments:
  globs                        test file globs, quote them so nole expands them

Options:
  -b, --bail                   stop as soon as something fails
  -t, --grep <pattern>         only run specs matching ClassName.specName,
                               /regex/ is supported
  --timeout <ms>               override the timeout of every spec and hook
  --retries <n>                extra attempts before a failing spec is reported
  -c, --concurrency <n>        how many test classes may run at once
  -r, --reporter <name>        spec, dot, json, tap or junit
  --report <reporter:file>     also write a report to a file, repeatable
  -s, --setup <file>           load a file before discovery, repeatable
  --config <file>              path to a nole config file
  --no-config                  ignore any nole.config.* file
  --wait <ms>                  delay before the run, for late dynamic tests
  --spec-skip-prefix <prefix>  methods with this prefix are helpers
  -w, --watch                  rerun when a file changes
  --coverage                   run the suite through c8
  --no-color                   disable coloured output
  -V, --version                output the version number
  -h, --help                   display help for command

Class options:

Test({
  skip: "reason",             // skip the class
  timeout: 30_000,            // ms per test and hook
  retries: 2,                 // extra attempts for a failing test
  bail: true,                 // stop this class at its first failure
  exclusive: true,            // never run next to another class
  dependencies: { db: () => DatabaseTest },
  before: () => [A],          // this class runs before A
  after: () => [B],           // this class runs after B
});

Your own reporter

// reporters/slack.ts
import { BaseReporter, type SpecResult } from "nole";

export class SlackReporter extends BaseReporter {
  specResult(result: SpecResult) {
    if (result.status === "failed") {
      notify(`${result.className}.${result.specName} failed`);
    }
  }
}

Override only what you need. Every event is optional.

Point the config file at it. Nole hands you the output stream.

// nole.config.ts
import { defineConfig } from "nole";
import { SlackReporter } from "./reporters/slack.js";

export default defineConfig({
  globs: ["./test/**/*.test.ts"],
  reporter: (write) => new SlackReporter(write),
});

Events, all optional:

runStart(info)      // the run is about to start
classStart(name)    // a class is about to start
hookResult(result)  // a hook finished
specResult(result)  // a test finished
retry(info)         // a test failed and will be tried again
classEnd(name)      // a class is done
runEnd(summary)     // everything is done
finalize()          // flush your buffers, may return a promise

License

MIT