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

@michaelhelvey/vitest-bdd

v1.0.2

Published

A BDD (Behavior-Driven Development) testing helper for Vitest that provides a structured given/when/it pattern with isolated world state for each test.

Readme

@michaelhelvey/vitest-bdd

A BDD (Behavior-Driven Development) testing helper for Vitest that provides a structured given/when/it pattern with isolated world state for each test.

Features

  • Natural language test structure - Write tests that read like specifications using given/when/it
  • Test isolation - Each it receives completely fresh world state, preventing test pollution
  • Lazy state creation - World state is created on-demand, allowing input modifications before instantiation
  • Async side-effects - Use $.perform() perform async side-effects (e.g., user interactions) after state creation but before assertions
  • Cleanup hooks - Register cleanup functions (e.g., unmounting React components) that run after each test
  • Skip and only modifiers - Use .skip and .only on given, when, and it to control test execution
  • Parameterized tests - Use .each for data-driven testing with printf-style formatting

Installation

npm install @michaelhelvey/vitest-bdd # or bun, yarn, pnpm, etc.

Peer Dependencies:

  • vitest ^3.2.4

Quick Start

import { given } from "@michaelhelvey/vitest-bdd";
import { expect } from "vitest";

class Counter {
  constructor(private _value = 0) {}
  inc() {
    this._value++;
  }
  get value() {
    return this._value;
  }
}

given(
  "a Counter",
  { initialValue: 0 },
  ({ initialValue }) => new Counter(initialValue),
  ({ when }) => {
    when(
      "initialized with value 5",
      ($) => {
        $.inputs.initialValue = 5;
      },
      ({ it }) => {
        it("has value 5", (counter) => {
          expect(counter.value).toEqual(5);
        });
      },
    );

    when(
      "incremented",
      ($) => {
        $.state.inc();
      },
      ({ it }) => {
        it("has value 1", (counter) => {
          expect(counter.value).toEqual(1);
        });
      },
    );
  },
);

API

given(scenario, inputs, createWorldState, tests)

The main function for creating BDD-style test suites.

| Parameter | Type | Description | | ------------------ | ---------------------------------- | ----------------------------------------------------- | | scenario | string | Description of the test context | | inputs | TInputs | Initial input values used to create world state | | createWorldState | (inputs: TInputs) => TWorldState | Factory function that creates world state from inputs | | tests | (helpers) => void | Function receiving { when, cleanup } helpers |

when(scenario, modifier, tests)

Defines a scenario within a given block.

| Parameter | Type | Description | | ---------- | ------------------ | -------------------------------------------------------- | | scenario | string | Description of the scenario | | modifier | ($) => void | Function to modify inputs or perform actions (see below) | | tests | ({ it }) => void | Function to define test assertions |

The modifier function receives an object with:

  • $.inputs - Proxy to modify input values before state creation
  • $.state - Lazily-created world state (accessing triggers creation)
  • $.perform(fn) - Register an async action to run after state creation

it(scenario, testFn)

Defines a test assertion within a when block.

| Parameter | Type | Description | | ---------- | ---------------------- | ----------------------------------------- | | scenario | string | Description of what the test asserts | | testFn | (worldState) => void | Test function receiving fresh world state |

cleanup(cleanupFn)

Registers a cleanup function to run after each test.

given(
  "...",
  {},
  () => createSomething(),
  ({ when, cleanup }) => {
    cleanup(() => destroySomething());
    // ...
  },
);

Skip and Only Modifiers

All three functions (given, when, it) support .skip and .only modifiers, mirroring vitest's behavior:

// Skip an entire feature
given.skip(
  "feature under development",
  {},
  () => ({}),
  ({ when }) => {
    // These tests won't run
  },
);

// Focus on a specific feature (only this runs)
given.only(
  "feature to debug",
  {},
  () => ({}),
  ({ when }) => {
    // Only these tests run
  },
);

// Skip a specific scenario
when.skip(
  "edge case not yet handled",
  ($) => {},
  ({ it }) => {},
);

// Focus on a specific scenario
when.only(
  "scenario to debug",
  ($) => {},
  ({ it }) => {},
);

// Skip individual tests
it.skip("not implemented yet", (state) => {});

// Focus on individual tests
it.only("debugging this test", (state) => {});

Parameterized Tests with .each

Use .each for data-driven testing. Supports printf-style formatting in descriptions:

| Format | Description | | ------ | -------------------- | | %s | String | | %d | Number | | %i | Integer | | %f | Float | | %j | JSON | | %o | Object | | %% | Literal percent sign |

given.each

given.each([
  [1, 2, 3],
  [2, 3, 5],
  [10, 20, 30],
])(
  "adding %d + %d = %d",
  (a, b, expected) => ({ a, b, expected }), // inputs factory
  ({ a, b }) => ({ sum: a + b }), // world state factory
  ({ when }) => {
    when(
      "computed",
      () => {},
      ({ it }) => {
        it("equals expected", ({ sum }, { expected }) => {
          expect(sum).toBe(expected);
        });
      },
    );
  },
);

when.each

given(
  "a calculator",
  { value: 0 },
  ({ value }) => new Calculator(value),
  ({ when }) => {
    when.each([
      [5, 5],
      [10, 10],
      [100, 100],
    ])(
      "adding %d",
      ($, amount, expected) => {
        $.state.add(amount);
      },
      ({ it }) => {
        it("has correct value", (calc) => {
          // Note: 'expected' from each() is available via closure if needed
        });
      },
    );
  },
);

it.each

when(
  "performing calculations",
  ($) => {},
  ({ it }) => {
    it.each([
      [1, 2, 3],
      [5, 5, 10],
      [-1, 1, 0],
    ])("adds %d + %d = %d", (a, b, expected, worldState) => {
      // worldState is always the LAST argument
      expect(a + b).toBe(expected);
    });
  },
);

Combining Modifiers

You can combine skip/only with each:

// Skip parameterized tests
it.skip.each([[1], [2], [3]])("test %d", (n, state) => {});

// Focus on parameterized tests
when.only.each([["a"], ["b"]])(
  "scenario %s",
  ($, letter) => {},
  ({ it }) => {},
);

Usage Examples

Testing a Class

import { given } from "@michaelhelvey/vitest-bdd";
import { expect } from "vitest";

class Calculator {
  constructor(private value = 0) {}
  add(n: number) {
    this.value += n;
  }
  getResult() {
    return this.value;
  }
}

given(
  "a Calculator",
  { initial: 0 },
  ({ initial }) => new Calculator(initial),
  ({ when }) => {
    when(
      "starting at 10",
      ($) => {
        $.inputs.initial = 10;
      },
      ({ it }) => {
        it("has initial value 10", (calc) => {
          expect(calc.getResult()).toEqual(10);
        });

        it("can add 5 to get 15", (calc) => {
          calc.add(5);
          expect(calc.getResult()).toEqual(15);
        });
      },
    );

    when(
      "5 is added",
      ($) => {
        $.state.add(5);
      },
      ({ it }) => {
        it("equals 5", (calc) => {
          expect(calc.getResult()).toEqual(5);
        });
      },
    );
  },
);

Testing React Components

import { given } from "@michaelhelvey/vitest-bdd";
import { render, cleanup as testingLibraryCleanup } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { expect } from "vitest";

function Counter({ start }: { start: number }) {
  const [count, setCount] = useState(start);
  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

given(
  "a Counter component",
  { start: 0 },
  (inputs) => render(<Counter start={inputs.start} />),
  ({ when, cleanup }) => {
    const user = userEvent.setup();
    cleanup(() => testingLibraryCleanup());

    when(
      "rendered with default props",
      () => {},
      ({ it }) => {
        it("shows count as 0", ({ getByTestId }) => {
          expect(getByTestId("count").innerText).toEqual("0");
        });
      },
    );

    when(
      "starting at 5 and clicking increment",
      ($) => {
        $.inputs.start = 5;
        $.perform(async () => {
          await user.click($.state.getByRole("button"));
        });
      },
      ({ it }) => {
        it("shows count as 6", ({ getByTestId }) => {
          expect(getByTestId("count").innerText).toEqual("6");
        });
      },
    );
  },
);

Key Concepts

Test Isolation

Every it test receives a completely fresh world state. This means:

when(
  "some scenario",
  ($) => {
    $.inputs.value = 5;
  },
  ({ it }) => {
    it("test A - mutates state", (state) => {
      state.mutate(); // This mutation...
    });

    it("test B - gets fresh state", (state) => {
      // ...does NOT affect this test. Fresh state here.
    });
  },
);

Input Modification Timing

Inputs can only be modified before accessing $.state:

when(
  "scenario",
  ($) => {
    $.inputs.value = 5; // OK - before state access
    $.state.doSomething(); // State created here
    $.inputs.value = 10; // ERROR! Cannot modify after state access
  },
  ({ it }) => {
    /* ... */
  },
);

The perform Function

Use $.perform() to register an action that runs after state creation but before test assertions:

when(
  "the button is clicked",
  ($) => {
    $.perform(async () => {
      await userEvent.click($.state.getByRole("button"));
    });
  },
  ({ it }) => {
    it("reflects the click", (state) => {
      // Assertions run after perform() completes
    });
  },
);

Note: perform() can only be called once per modifier.

License

MIT