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

test-fns

v1.15.0

Published

write usecase driven tests systematically for simpler, safer, and more readable code

Readme

test-fns

ci_on_commit deploy_on_tag

write usecase driven tests systematically for simpler, safer, and more readable code

purpose

establishes a pattern to write tests for simpler, safer, and more readable code.

by tests defined in terms of usecases (given, when, then) your tests are

  • simpler to write
  • easier to read
  • safer to trust

install

npm install --save-dev test-fns

pattern

given/when/then is based on behavior driven design (BDD). it structures tests around:

  • given a scene (the initial state or context)
  • when an event occurs (the action or trigger)
  • then an effect is observed (the expected outcome)
given('scene', () =>
  when('event', () =>
    then('effect', () => {
      // assertion
    })
  )
);

use

jest

import { given, when, then } from 'test-fns';

describe('doesPlantNeedWater', () => {
  given('a dry plant', () => {
    const plant = { id: 7, hydration: 'DRY' };

    when('water needs are checked', () => {
      then('it should return true', () => {
        expect(doesPlantNeedWater(plant)).toEqual(true);
      });
    });
  });
});

vitest

vitest requires a workaround because ESM's thenable protocol prevents direct then imports.

option 1: globals via setup file (recommended)

// vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ['test-fns/vitest.setup'],
  },
});

// your test file - no imports needed
describe('doesPlantNeedWater', () => {
  given('a dry plant', () => {
    const plant = { id: 7, hydration: 'DRY' };

    when('water needs are checked', () => {
      then('it should return true', () => {
        expect(doesPlantNeedWater(plant)).toEqual(true);
      });
    });
  });
});

option 2: bdd namespace

import { bdd } from 'test-fns';

describe('doesPlantNeedWater', () => {
  bdd.given('a dry plant', () => {
    const plant = { id: 7, hydration: 'DRY' };

    bdd.when('water needs are checked', () => {
      bdd.then('it should return true', () => {
        expect(doesPlantNeedWater(plant)).toEqual(true);
      });
    });
  });
});

output

both produce:

 PASS  src/plant.test.ts
  doesPlantNeedWater
    given: a dry plant
      when: water needs are checked
        ✓ then: it should return true (1 ms)

features

.runIf(condition) && .skipIf(condition)

skip the suite if the condition is not met

describe('your test', () => {
  given.runIf(onLocalMachine)('some test that should only run locally', () => {
    then.skipIf(onProduction)('some test that should not run against production', () => {
      expect(onProduction).toBeFalse()
    })
  })
})

.repeatably(config)

run a block multiple times to evaluate repeatability. available on given, when, and then.

then.repeatably — run a test multiple times with environment-aware criteria

then.repeatably({
  attempts: 3,
  criteria: process.env.CI ? 'SOME' : 'EVERY',
})('it should produce consistent results', ({ attempt }) => {
  const result = generateOutput();
  expect(result).toMatchSnapshot();
});
  • attempts: how many times to run the test
  • criteria:
    • 'EVERY': all attempts must pass (strict, for local development)
    • 'SOME': at least one attempt must pass (tolerant, for CI)

when.repeatably — run the when block multiple times

given('a probabilistic llm system', () => {
  when.repeatably({
    attempts: 3,
    criteria: 'SOME', // pass if any attempt succeeds
  })('the llm generates a response', ({ attempt }) => {
    then('it should produce valid json', () => {
      const result = generateResponse();
      expect(() => JSON.parse(result)).not.toThrow();
    });
  });
});

given.repeatably — run the given block multiple times

given.repeatably({
  attempts: 3,
  criteria: 'EVERY', // all attempts must pass (default)
})('different initial states', ({ attempt }) => {
  const state = setupState(attempt);

  when('the system processes the state', () => {
    then('it should handle all variations', () => {
      expect(process(state)).toBeDefined();
    });
  });
});

all repeatably variants:

  • provide an { attempt } parameter (starts at 1) to the callback
  • support criteria: 'EVERY' | 'SOME' (defaults to 'EVERY')
    • 'EVERY': all attempts must pass
    • 'SOME': at least one attempt must pass (useful for probabilistic tests)

full block retry with criteria: 'SOME'

when given.repeatably or when.repeatably uses criteria: 'SOME', the entire block is retried when any then block fails:

when.repeatably({
  attempts: 3,
  criteria: 'SOME',
})('llm generates valid output', ({ attempt }) => {
  // if thenB fails, BOTH thenA and thenB will run again on the next attempt
  then('thenA: output is not empty', () => {
    expect(result.output.length).toBeGreaterThan(0);
  });

  then('thenB: output is valid json', () => {
    expect(() => JSON.parse(result.output)).not.toThrow();
  });
});

this enables reliable tests for probabilistic systems where multiple assertions must pass together. if attempt 1 fails on thenB, attempt 2 will re-run both thenA and thenB from scratch.

skip-on-success behavior

once any attempt passes (all then blocks succeed), subsequent attempts are skipped entirely:

  • all then blocks are skipped
  • useBeforeAll and useAfterAll callbacks are skipped
  • expensive setup operations do not execute

recommended pattern for ci/cd

for reliable ci/cd with probabilistic tests (like llm-powered systems), use environment-aware criteria:

const criteria = process.env.CI ? 'SOME' : 'EVERY';

when.repeatably({ attempts: 3, criteria })('llm generates response', ({ attempt }) => {
  then('response is valid', () => {
    // strict at devtime (EVERY): all 3 attempts must pass
    // tolerant at cicdtime (SOME): at least 1 attempt must pass
    expect(response).toMatchSnapshot();
  });
});

this pattern provides:

  • strict validation at devtime'EVERY' ensures consistent behavior across all attempts
  • reliable ci/cd pipelines'SOME' tolerates occasional probabilistic failures while still able to catch systematic issues

hooks

similar to the sync-render constraints that drove react to leverage hooks, we leverage hooks in tests due to those same constraints. test frameworks collect test definitions synchronously, then execute them later. hooks let immutable references to data be declared before the data is rendered via execution — which enables const declarations instead of let mutations.

useBeforeAll

prepare test resources once for all tests in a suite, to optimize setup time for expensive operations

describe('spaceship refuel system', () => {
  given('a spaceship that needs to refuel', () => {
    const spaceship = useBeforeAll(async () => {
      // runs once before all tests in this suite
      const ship = await prepareExampleSpaceship();
      await ship.dock();
      return ship;
    });

    when('[t0] no changes yet', () => {
      then('it should be docked', async () => {
        expect(spaceship.isDocked).toEqual(true);
      });

      then('it should need fuel', async () => {
        expect(spaceship.fuelLevel).toBeLessThan(spaceship.fuelCapacity);
      });
    });

    when('[t1] it connects to the fuel station', () => {
      const result = useBeforeAll(async () => await spaceship.connectToFuelStation());

      then('it should be connected', async () => {
        expect(result.connected).toEqual(true);
      });

      then('it should calculate required fuel', async () => {
        expect(result.fuelNeeded).toBeGreaterThan(0);
      });
    });
  });
});

useBeforeEach

prepare fresh test resources before each test to ensure test isolation

describe('spaceship combat system', () => {
  given('a spaceship in battle', () => {
    // runs before each test to ensure a fresh spaceship
    const spaceship = useBeforeEach(async () => {
      const ship = await prepareExampleSpaceship();
      await ship.resetShields();
      return ship;
    });

    when('[t0] no changes yet', () => {
      then('it should have full shields', async () => {
        expect(spaceship.shields).toEqual(100);
      });

      then('it should be ready for combat', async () => {
        expect(spaceship.status).toEqual('READY');
      });
    });

    when('[t1] it takes damage', () => {
      const result = useBeforeEach(async () => await spaceship.takeDamage(25));

      then('it should reduce shield strength', async () => {
        expect(spaceship.shields).toEqual(75);
      });

      then('it should return damage report', async () => {
        expect(result.damageReceived).toEqual(25);
      });
    });
  });
});

when to use each:

  • useBeforeAll: use when setup is expensive (database connections, api calls) and tests don't modify the resource
  • useBeforeEach: use when tests modify the resource and need isolation between runs

useThen

capture the result of an operation in a then block and share it with sibling then blocks, without let declarations

describe('invoice system', () => {
  given('a customer with an overdue invoice', () => {
    when('[t1] the invoice is processed', () => {
      const result = useThen('process succeeds', async () => {
        return await processInvoice({ customerId: '123' });
      });

      then('it should mark the invoice as sent', () => {
        expect(result.status).toEqual('sent');
      });

      then('it should calculate the correct total', () => {
        expect(result.total).toEqual(150.00);
      });

      then('it should include the late fee', () => {
        expect(result.lateFee).toEqual(25.00);
      });
    });
  });
});

useThen creates a test (then block) and returns a proxy to the result. the proxy defers access until the test runs, which makes the result available to sibling then blocks.

useWhen

capture the result of an operation at the given level and share it with sibling when blocks — ideal for idempotency verification

describe('user registration', () => {
  given('a new user email', () => {
    when('[t0] before any changes', () => {
      then('user does not exist', async () => {
        const user = await findUser({ email: '[email protected]' });
        expect(user).toBeNull();
      });
    });

    const responseFirst = useWhen('[t1] registration is called', () => {
      const response = useThen('registration succeeds', async () => {
        return await registerUser({ email: '[email protected]' });
      });

      then('user is created', () => {
        expect(response.status).toEqual('created');
      });

      return response;
    });

    when('[t2] registration is repeated', () => {
      const responseSecond = useThen('registration still succeeds', async () => {
        return await registerUser({ email: '[email protected]' });
      });

      then('response is idempotent', () => {
        expect(responseSecond.id).toEqual(responseFirst.id);
        expect(responseSecond.status).toEqual(responseFirst.status);
      });
    });
  });
});

useWhen executes during test collection and returns a value accessible to sibling when blocks. use it with useThen inside to capture async operation results for cross-block comparisons like idempotency verification.

how to choose the right hook

| hook | when to use | execution timing | | --------------- | ------------------------------------------------ | ---------------------- | | useBeforeAll | expensive setup shared across tests | once before all tests | | useBeforeEach | setup that needs isolation | before each test | | useThen | capture async operation result in a test | during test execution | | useWhen | wrap a when block and share result with siblings | during test collection |

key differences:

  • useBeforeAll/useBeforeEach - for test fixtures and setup
  • useThen - for operations that ARE the test (creates a then block)
  • useWhen - wraps a when block at given level, returns result for sibling when blocks (idempotency verification)

immutability benefits

these hooks enable immutable test code:

// ❌ mutable - requires let
let result;
beforeAll(async () => {
  result = await fetchData();
});
it('uses result', () => {
  expect(result.value).toBe(1);
});

// ✅ immutable - const only
const result = useBeforeAll(async () => await fetchData());
then('uses result', () => {
  expect(result.value).toBe(1);
});

benefits of immutability:

  • safer: no accidental reassignment or mutation
  • clearer: data flow is explicit
  • simpler: no need to track when variables are assigned

utilities

genTempDir

generates a temporary test directory within the repo's .temp folder, with automatic cleanup of stale directories.

features:

  • portable across os systems (no os-specific temp dir dependencies)
  • timestamp-prefixed names enable age-based cleanup
  • slug in directory name helps identify which test created it
  • auto-prunes directories older than 7 days
  • optional fixture clone for pre-populated test scenarios

basic usage:

import { genTempDir } from 'test-fns';

describe('file processor', () => {
  given('a test directory', () => {
    const testDir = genTempDir({ slug: 'file-processor' });

    when('files are written', () => {
      then('they exist in the test directory', async () => {
        await fs.writeFile(path.join(testDir, 'example.txt'), 'content');
        expect(await fs.stat(path.join(testDir, 'example.txt'))).toBeDefined();
      });
    });
  });
});

with fixture clone:

import { genTempDir } from 'test-fns';

describe('config parser', () => {
  given('a directory with config files', () => {
    const testDir = genTempDir({
      slug: 'config-parser',
      clone: './src/__fixtures__/configs',
    });

    when('config is loaded', () => {
      then('it parses correctly', async () => {
        const config = await loadConfig(testDir);
        expect(config.configOption).toEqual('value');
      });
    });
  });
});

with symlinks to repo root:

import { genTempDir } from 'test-fns';

describe('package installer', () => {
  given('a temp directory with symlinks to repo artifacts', () => {
    const testDir = genTempDir({
      slug: 'installer-test',
      symlink: [
        { at: 'node_modules', to: 'node_modules' },
        { at: 'config/tsconfig.json', to: 'tsconfig.json' },
      ],
    });

    when('the installer runs', () => {
      then('it can access linked dependencies', async () => {
        const nodeModules = path.join(testDir, 'node_modules');
        expect(await fs.stat(nodeModules)).toBeDefined();
      });
    });
  });
});

symlink options:

  • at = relative path within the temp dir (where symlink is created)
  • to = relative path within the repo root (what symlink points to)

notes:

  • symlinks are created after clone (if both specified)
  • parent directories are created automatically for nested at paths
  • throws BadRequestError if target does not exist
  • throws BadRequestError if symlink path collides with cloned content

with git initialization:

import { genTempDir } from 'test-fns';

describe('git status helper', () => {
  given('a git repo with committed baseline', () => {
    const testDir = genTempDir({
      slug: 'git-status-test',
      clone: './src/__fixtures__/project',
      git: true,
    });

    when('a file is modified', () => {
      fs.appendFileSync(path.join(testDir, 'config.json'), '\n// comment');

      then('git diff detects the change', () => {
        const diff = execSync('git diff', { cwd: testDir }).toString();
        expect(diff).toContain('+// comment');
      });
    });
  });
});

git options:

  • git: true — init repo, commit 'began', clone/symlink, commit 'fixture'
  • git: { commits: { init: false } } — init repo only, no commits
  • git: { commits: { fixture: false } } — commit 'began' only, leave clone/symlink uncommitted

notes:

  • repo-local git config is set (ci-safe, no global config needed)
  • 'began' commit is empty (created before clone/symlinks)
  • 'fixture' commit contains all clone/symlink content
  • if no clone/symlink provided, only 'began' commit is created

directory format:

.temp/2026-01-19T12-34-56.789Z.my-test.a1b2c3d4/
      └── {timestamp}.{slug}.{8-char-uuid}

the slug helps debuggers identify which test created a directory when they debug.

cleanup behavior:

directories in .temp are automatically pruned when:

  • they are older than 7 days (based on timestamp prefix)
  • genTempDir() is called (prune runs in background)

the .temp directory includes a readme.md that explains the ttl policy.

isTempDir

checks if a path is a test directory created by genTempDir.

import { isTempDir } from 'test-fns';

isTempDir({ path: '/repo/.temp/2026-01-19T12-34-56.789Z.my-test.a1b2c3d4' }); // true
isTempDir({ path: '/tmp/random' }); // false

slowtest reporter

identify slow tests in your test suite with hierarchical time visibility.

what it does

the slowtest reporter runs after your tests complete and shows:

  • which test files are slow (above a configurable threshold)
  • nested hierarchy breakdown with time spent in each given/when/then block
  • hook time (setup/teardown) separated from test execution time

jest configuration

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  reporters: [
    'default',
    ['test-fns/slowtest.reporter.jest', {
      slow: '3s',                        // threshold (default: 3s for unit, 10s for integration)
      output: '.slowtest/report.json',   // optional: export json report
      top: 10,                           // optional: limit terminal output to top N slow files
    }],
  ],
};

export default config;

vitest configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import SlowtestReporter from 'test-fns/slowtest.reporter.vitest';

export default defineConfig({
  test: {
    reporters: [
      'default',
      new SlowtestReporter({
        slow: '3s',
        output: '.slowtest/report.json',
      }),
    ],
  },
});

terminal output

after tests complete, you'll see a report like:

slowtest report:
----------------------------------------------------------------------
🐌 src/invoice/invoice.test.ts                              8s 710ms [SLOW]
   └── given: [case1] overdue invoice                       7s 230ms
       ├── (hooks: 340ms)
       └── when: [t0] nurture triggered                     6s 890ms
           ├── then: sends reminder email                   6s 10ms
           └── then: logs notification                      880ms

🐌 src/auth/login.test.ts                                   3s 500ms [SLOW]

----------------------------------------------------------------------
total: 15s 10ms
files: 4
slow: 2 file(s) above threshold

the report shows:

  • 🐌 emoji marks slow files
  • hierarchical breakdown with given > when > then structure
  • hook time displayed as (hooks: Xms) when setup contributes to block duration
  • summary with total time, file count, and slow file count

configuration options

| option | type | default | description | |--------|------|---------|-------------| | slow | number \| string | 3000 (3s) | threshold in ms or human-readable string ('3s', '500ms') | | output | string | — | path to write json report (e.g., .slowtest/report.json) | | top | number | — | limit terminal output to top N slowest files |

json output format

when output is configured, the reporter writes a json file:

{
  "generated": "2026-01-31T14:23:00Z",
  "summary": {
    "total": 15010,
    "files": 4,
    "slow": 2
  },
  "files": [
    {
      "path": "src/invoice/invoice.test.ts",
      "duration": 8710,
      "slow": true,
      "blocks": [
        {
          "type": "given",
          "name": "[case1] overdue invoice",
          "duration": 7230,
          "hookDuration": 340,
          "blocks": [
            {
              "type": "when",
              "name": "[t0] nurture triggered",
              "duration": 6890,
              "hookDuration": 0,
              "tests": [
                { "name": "then: sends reminder email", "duration": 6010 },
                { "name": "then: logs notification", "duration": 880 }
              ]
            }
          ]
        }
      ]
    }
  ]
}

use the json output for:

  • trend analysis over time
  • ci shard optimization (distribute tests by time)
  • integration with other tools