🚀 Big News:Socket Has Acquired Secure Annex.Learn More →
Socket
Book a DemoSign in
Socket

ts-bdd

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ts-bdd

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

latest
Source
npmnpm
Version
1.1.2
Version published
Maintainers
1
Created
Source

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

License

ISC

Keywords

bdd

FAQs

Package last updated on 04 Aug 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts