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

ts-bdd

v1.1.2

Published

A TypeScript BDD testing framework with typed shared examples and state management

Readme

ts-bdd

npm version npm downloads

A type-safe BDD testing library for TypeScript that provides lazy variable definitions, shared examples, and subjects with full async support.

Installation

npm install ts-bdd
yarn add ts-bdd

Breaking Changes in v2.0.0

⚠️ Breaking Change: The it function is no longer provided by the TestRunner interface or passed to suite callbacks.

Migration: Import it (and other test functions) directly from your test framework:

// Before v2.0.0
const runner = { describe, it, beforeEach };
suite.describe('Tests', ({ get, set, it }) => {
  // Used 'it' from suite callback
});

// v2.0.0+
import { it } from 'vitest'; // or jest, etc.
const runner = { describe, beforeEach }; // No 'it' needed
suite.describe('Tests', ({ get, set }) => {
  it('works', () => {
    /* test */
  }); // Use imported 'it'
});

This change simplifies the API and eliminates unnecessary dependency injection for functions that weren't used internally.

Features

  • Type-safe: Full TypeScript support with proper type inference
  • Lazy definitions: Variables computed on-demand with caching
  • Shared examples: Reusable test behaviors with type safety
  • Subjects: Non-caching factories for testing side effects
  • Async support: Full support for async operations
  • Framework agnostic: Works with any test runner (vitest, jest, etc.)

Basic Usage

import { createSuite } from 'ts-bdd';
import { describe, it, beforeEach } from 'vitest';

interface AppState {
  config: { apiUrl: string; timeout: number };
  client: HttpClient;
}

const suite = createSuite({
  definitions: {
    config: { apiUrl: 'https://api.example.com', timeout: 5000 },
    client: (get) => new HttpClient(get('config')),
  },
  runner: { describe, beforeEach }, // Note: 'it' is imported directly above
});

suite.describe('API Client', ({ get, set, context }) => {
  it('should create client with config', () => {
    const client = get('client');
    expect(client.timeout).toBe(5000);
  });

  context('with custom timeout', () => {
    set('config', { apiUrl: 'https://api.example.com', timeout: 10000 });

    it('should use custom timeout', () => {
      const client = get('client');
      expect(client.timeout).toBe(10000);
    });
  });
});

Async Support

The library provides comprehensive async support:

1. Async Test Functions

Test functions can be async (this is standard vitest/jest behavior):

suite.describe('Async Tests', ({ get }) => {
  it('should handle async operations', async () => {
    const result = await someAsyncOperation();
    expect(result).toBe('success');
  });
});

2. Async Lazy Definitions

Lazy definitions can be async functions that return promises:

interface AsyncState {
  userId: number;
  userData: Promise<User>; // Note: Promise type in interface
  posts: Promise<Post[]>;
}

const suite = createSuite({
  definitions: {
    userId: 42,
    userData: async (get) => {
      const id = get('userId');
      return await fetchUser(id); // Returns Promise<User>
    },
    posts: async (get) => {
      const id = get('userId');
      return await fetchUserPosts(id); // Returns Promise<Post[]>
    },
  },
  runner,
});

suite.describe('Async Data', ({ get }) => {
  it('should fetch user data', async () => {
    const userData = await get('userData');
    expect(userData.name).toBeTruthy();
  });

  it('should cache async promises', async () => {
    const promise1 = get('userData');
    const promise2 = get('userData');

    // Same promise instance is returned (cached)
    expect(promise1).toBe(promise2);

    const [user1, user2] = await Promise.all([promise1, promise2]);
    expect(user1).toEqual(user2);
  });
});

3. Async Subjects

Subjects can have async factories:

suite.describe('Async Subjects', ({ get, subject }) => {
  it('should handle async subject factories', async () => {
    subject(async () => {
      const userData = await get('userData');
      return { processedUser: userData.name, timestamp: Date.now() };
    });

    const result1 = await subject();
    expect(result1.processedUser).toBeTruthy();

    // Subject executes factory each time (no caching)
    await new Promise((resolve) => setTimeout(resolve, 1));
    const result2 = await subject();
    expect(result2.timestamp).toBeGreaterThan(result1.timestamp);
  });
});

4. Async Shared Examples

Shared example functions can be async and are created using the builder:

suite.describe(
  'Async Shared Examples',
  ({ get, set, context, sharedExamplesBuilder }) => {
    const itBehavesLike = sharedExamplesBuilder
      .add('async validation', ({ subject }) => {
        it('should validate async data', async () => {
          const userData = await get('userData'); // Access outer scope get
          set('validationResult', { isValid: true, user: userData }); // Access outer scope set

          expect(userData.id).toBeGreaterThan(0);
          expect(userData.name).toBeTruthy();
        });
      })
      .build();

    context('with async data', () => {
      itBehavesLike('async validation');
    });
  },
);

Important Notes on Async Support

Suite Callbacks Must Be Synchronous

While test functions, lazy definitions, subjects, and shared examples can be async, the main suite callback passed to describe() must be synchronous. This is a limitation of most test runners:

// ❌ This won't work - test runner expects synchronous callback
suite.describe('Suite', async ({ get, it }) => {
  await someSetup(); // This won't be awaited properly
  it('test', () => {
    /* ... */
  });
});

// ✅ This works - async operations inside test functions
suite.describe('Suite', ({ get, it }) => {
  it('test with async operations', async () => {
    await someSetup(); // This works fine
    const result = await get('asyncData');
    expect(result).toBeTruthy();
  });
});

Context Callbacks Are Also Synchronous

Similar to suite callbacks, context callbacks must be synchronous:

context('async context', () => {
  // Synchronous setup only
  it('async test', async () => {
    // Async operations work here
    const result = await get('asyncData'); // Access outer scope get
    expect(result).toBeTruthy();
  });
});

Type Inference with Async Definitions

When using async lazy definitions, you may need to explicitly type your state interface:

// Define the resolved types, not the Promise types
interface MyState {
  userData: Promise<User>; // The actual type returned by get()
}

// Or use type assertions in tests
const userData = (await get('userData')) as User;

Advanced Features

Multi-argument get()

const [user, posts, config] = get('userData', 'posts', 'config');

Shared Examples with Arguments and Inheritance

import { createSuite, SharedExamplesBuilder } from 'ts-bdd';

// Create shared examples using the builder pattern
suite.describe(
  'Shared Examples Demo',
  ({ get, set, context, subject, sharedExamplesBuilder }) => {
    const itBehavesLike = sharedExamplesBuilder
      .add('basic validation', ({ subject }) => {
        it('should be valid', () => {
          expect(subject()).toBeTruthy();
        });
      })
      .add('validation with argument', (expectedValue: string, { subject }) => {
        it(`should equal ${expectedValue}`, () => {
          expect(subject()).toBe(expectedValue);
        });
      })
      .add('extended validation', ({ subject, itBehavesLike }) => {
        itBehavesLike('basic validation'); // Inherit behavior

        it('should have additional properties', () => {
          expect(subject().extra).toBeDefined();
        });
      })
      .build();

    // Use shared examples
    context('with valid data', () => {
      subject(() => ({ value: 'test', extra: true }));

      itBehavesLike('basic validation');
      itBehavesLike('validation with argument', 'test');
      itBehavesLike('extended validation');
    });
  },
);

Subjects for Side Effects

Subjects are perfect for testing operations with side effects since they don't cache results:

it('should handle side effects', async () => {
  let counter = 0;

  subject(async () => {
    counter++;
    const data = await get('userData');
    return { count: counter, user: data.name };
  });

  const result1 = await subject();
  const result2 = await subject();

  expect(result1.count).toBe(1);
  expect(result2.count).toBe(2); // Factory executed again
});

API Reference

createSuite<TState>(options)

Creates a new test suite builder.

Parameters:

  • options.definitions: Object defining the state variables
  • options.runner: Test runner interface ({ describe, beforeEach })

Returns: SuiteBuilder<TState>

SharedExamplesBuilder<TState>

Available within suite callbacks via the sharedExamplesBuilder parameter. Use the builder pattern to define reusable test behaviors:

// Import 'it' directly from your test framework
import { it } from 'vitest';

suite.describe('Test Suite', ({ sharedExamplesBuilder }) => {
  const itBehavesLike = sharedExamplesBuilder
    .add('behavior name', (optionalArg, { subject, itBehavesLike }) => {
      it('should behave correctly', () => {
        // Define shared behavior using imported 'it'
        // Access outer scope functions like get(), set() when needed
      });
    })
    .build();
});

Suite Callback Parameters

  • get: Function to retrieve state values
  • set: Function to override state values
  • context: Function to create nested contexts
  • itBehavesLike: Function to include shared examples (only in shared examples)
  • subject: Function to define/get non-caching factories
  • sharedExamplesBuilder: Builder for creating typed shared examples

Note: Import test functions like it, expect, etc. directly from your test framework (vitest, jest, etc.)

Repository

  • GitHub: https://github.com/amirketter/ts-bdd
  • npm: https://www.npmjs.com/package/ts-bdd
  • Issues: https://github.com/amirketter/ts-bdd/issues

License

ISC