
Security News
Socket Releases Free Certified Patches for Critical vm2 Sandbox Escape
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.
A TypeScript BDD testing framework with typed shared examples and state management
A type-safe BDD testing library for TypeScript that provides lazy variable definitions, shared examples, and subjects with full async support.
npm install ts-bdd
yarn add ts-bdd
⚠️ 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.
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);
});
});
});
The library provides comprehensive async support:
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');
});
});
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);
});
});
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);
});
});
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');
});
},
);
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();
});
});
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();
});
});
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;
const [user, posts, config] = get('userData', 'posts', 'config');
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 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
});
createSuite<TState>(options)Creates a new test suite builder.
Parameters:
options.definitions: Object defining the state variablesoptions.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();
});
get: Function to retrieve state valuesset: Function to override state valuescontext: Function to create nested contextsitBehavesLike: Function to include shared examples (only in shared examples)subject: Function to define/get non-caching factoriessharedExamplesBuilder: Builder for creating typed shared examplesNote: Import test functions like it, expect, etc. directly from your test framework (vitest, jest, etc.)
ISC
FAQs
A TypeScript BDD testing framework with typed shared examples and state management
We found that ts-bdd demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

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.

Security News
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.

Research
Five malicious NuGet packages impersonate Chinese .NET libraries to deploy a stealer targeting browser credentials, crypto wallets, SSH keys, and local files.

Security News
pnpm 11 turns on a 1-day Minimum Release Age and blocks exotic subdeps by default, adding safeguards against fast-moving supply chain attacks.