bdd-lazy-var-next
v0.0.5
Published
Provides helpers for testing frameworks such as mocha/jasmine/jest/vitest/bun which allows to define lazy variables and subjects
Maintainers
Readme
BDD + lazy variable definition (aka rspec)
Note: This is a fork of the original bdd-lazy-var with added support for Vitest and Bun test frameworks.
Provides helpers for testing frameworks such as bun:test, vitest, jest, mocha and jasmine which allows to define lazy variables and subjects.
⚠️ [!WARNING] > Breaking Changes from
bdd-lazy-varIf you are migrating from the original library, please note the following critical changes:
- No Auto-Detection: You must import from the specific framework entry point (e.g.,
bdd-lazy-var-next/bun,bdd-lazy-var-next/jest). The genericbdd-lazy-varimport is not supported.- Explicit Imports Only: Global variables (
def,get,subject) are no longer supported. You must explicit import them.- Native ESM: This library is published as native ESM. Ensure your environment supports ESM.
- Removed
itsShortcut: Theitsshortcut andis.expectedhelper have been removed to simplify the API and reduce maintenance. Use standarditblocks with assertions instead.
Installation
npm install bdd-lazy-var-next --save-dev
# or
bun add -d bdd-lazy-var-nextSetup & Configuration
Important: Unlike the original bdd-lazy-var, this library requires you to import the specific entry point for your testing framework.
Bun
import { get, def, subject } from "bdd-lazy-var-next/bun";
describe("My Bun Test", () => {
def("value", () => 1);
// ...
});Bun Test Isolation
Bun runs all test files in a shared global context. This means global variable definitions (e.g., def('foo')) can collide across files, causing errors like "Cannot define variable twice".
Solutions:
- Define variables inside
describeonly - Use unique variable names or suite names in each test file.
Example:
Vitest
Note: You must set globals: true in your Vitest configuration for bdd-lazy-var-next to work correctly. This is because the library relies on global lifecycle hooks provided by Vitest (like beforeAll, afterAll).
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
},
});// test/example.test.ts
import { get, def } from "bdd-lazy-var-next/vitest";
describe("My Vitest Test", () => {
// ...
});Example:
Jest
🚨 CAUTION: Do yourself a favor and migrate to Vitest or Bun test. Jest is slow, has poor ESM support, and is no longer actively innovated. Don't use Jest.
Important: You must use the global describe, it, test, and expect variables provided by Jest. Importing them from @jest/globals is not supported and will cause "Cannot define variable twice" errors because the library cannot intercept those imports to add its tracking logic.
import { get, def } from "bdd-lazy-var-next/jest";Example:
Mocha
🚨 CAUTION: Do yourself a favor and migrate to Vitest or Bun test. Mocha is outdated and lacks modern features like native ESM support, built-in TypeScript, and parallel testing. Don't use Mocha.
import { get, def } from "bdd-lazy-var-next/mocha";Jasmine
🚨 CAUTION: Do yourself a favor and migrate to Vitest or Bun test. Jasmine is legacy technology with poor ESM support and minimal modern tooling integration. Don't use Jasmine.
import { get, def } from "bdd-lazy-var-next/jasmine";Usage Guide
Basic Usage
The core concept is defining variables that are lazily evaluated and automatically cleaned up.
import { get, def } from "bdd-lazy-var-next/bun"; // or /vitest, /jest, /mocha, /jasmine
describe("Suite", () => {
// Define a variable 'name'
def("name", () => `John Doe ${Math.random()}`);
it("defines `name` variable", () => {
// Access it using get()
expect(get("name")).to.exist;
});
it("does not use name, so it is not created", () => {
expect(1).to.equal(1);
});
});Lazy Evaluation
Variables are instantiated only when referenced. That means if you don't use variable inside your test it won't be evaluated, making your tests run faster.
Composition
Due to laziness we are able to compose variables. This allows to define more general variables at the top level and more specific at the bottom:
describe('User', function() {
subject('user', () => new User(get('props')))
describe('when user is "admin"', function() {
def('props', () => ({ role: 'admin' }))
it('can update articles', function() {
// user is created with property role equal "admin"
expect(get('user')).to....
})
})
describe('when user is "member"', function() {
def('props', () => ({ role: 'member' }))
it('cannot update articles', function() {
// user is created with property role equal "member"
expect(get('user')).to....
})
})
})Named Subjects
You can give your subject a name to reference it explicitly, or use the default subject alias.
describe("Array", () => {
subject("collection", () => [1, 2, 3]);
it("has 3 elements by default", () => {
expect(get("subject")).to.equal(get("collection"));
expect(get("collection")).to.have.length(3);
});
});Advanced Features
Shared Examples
Very often you may find that some behavior repeats (e.g., when you implement Adapter pattern), and you would like to reuse tests for a different class or object.
sharedExamplesFor- defines a set of reusable tests.includeExamplesFor- runs previously defined examples in current context (i.e., in currentdescribe).itBehavesLike- runs defined examples in nested context (i.e., in nesteddescribe).
WARNING: files containing shared examples must be loaded before the files that use them.
sharedExamplesFor("a collection", (size) => {
it("has correct size", () => {
expect(get("subject").size).to.equal(size);
});
});
describe("Set", () => {
subject(() => new Set([1, 2, 7]));
itBehavesLike("a collection", 3);
});
describe("Map", () => {
subject(() => new Map([[2, 1]]));
itBehavesLike("a collection", 1);
});TypeScript Support
bdd-lazy-var-next includes full TypeScript support with generic types for type-safe variable definitions and access.
Configuration
The library uses package.json exports field to provide framework-specific entry points (./bun, ./jest, ./vitest, etc.). Your TypeScript configuration needs to support this.
For Node.js / Backend Projects (Recommended)
Use NodeNext for the most complete and reliable support:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"isolatedModules": true
// ... other options
}
// ... other options
}Why NodeNext?
- Fully supports
package.jsonexportsfield with conditional exports - Correctly resolves the
typesfield for subpath imports likebdd-lazy-var-next/jest - Best option for Node.js, Bun, Jest, Mocha testing environments
For Frontend / Bundler Projects
If you're using Vite, Webpack, or other bundlers (e.g., for Vitest in a frontend app), you can use:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true
// ... other options
}
// ... other options
}Note: bundler moduleResolution should work for most cases, but if you encounter type resolution issues with subpath imports, switch to NodeNext.
Basic Usage
When using explicit imports, TypeScript loads corresponding declarations automatically:
import { get, def } from "bdd-lazy-var-next/bun";Type-Safe Variables
All functions (def, get, subject) support TypeScript generics for type safety.
Method 1: Explicit Type Parameters (Recommended)
Specify the type when calling get() for full type safety:
import { def, get } from "bdd-lazy-var-next/bun";
// Define your types
interface User {
name: string;
age: number;
email: string;
}
// Define variables (types are optional here)
def("userName", () => "John Doe");
def("user", () => ({
name: "John Doe",
age: 30,
email: "[email protected]",
}));
def("scores", () => [95, 87, 92, 88]);
// Get variables with explicit type parameters
const userName = get<string>("userName");
console.log(userName.toUpperCase()); // Type-safe string methods ✓
const user = get<User>("user");
console.log(user.email); // Type-safe property access ✓
const scores = get<number[]>("scores");
const average = scores.reduce((a, b) => a + b, 0) / scores.length; // ✓Method 2: Variable Type Annotations
Let TypeScript infer from your variable declaration:
// TypeScript infers the type from the annotation
const userName: string = get("userName");
console.log(userName.toUpperCase()); // Works!
const user: User = get("user");
console.log(user.email); // Works!
const scores: number[] = get("scores");
console.log(scores.length); // Works!Typing def() (Optional)
You can also add types to def() for consistency:
// Type the definition function
def<string>("userName", () => "John Doe");
def<User>("user", () => ({
name: "John Doe",
age: 30,
email: "[email protected]",
}));
def<number[]>("scores", () => [95, 87, 92, 88]);Type-Safe subject()
Define and access subjects with types:
import { subject } from "bdd-lazy-var-next/bun";
describe("User", () => {
// Named subject with type
subject<User>("currentUser", () => ({
name: "Jane Smith",
age: 25,
email: "[email protected]",
}));
it("has correct properties", () => {
// Access with type safety
const user = subject<User>();
expect(user.name).toBe("Jane Smith");
});
});
describe("Array operations", () => {
// Anonymous subject with type
subject<number[]>(() => [1, 2, 3, 4, 5]);
it("calculates sum", () => {
const numbers = subject<number[]>();
const sum = numbers.reduce((a, b) => a + b, 0);
expect(sum).toBe(15);
});
});Advanced Type Usage
You can use any TypeScript type, including generics and unions:
// Generic types
def<Record<string, number>>("scores", () => ({
math: 95,
science: 87,
english: 92,
}));
// Union types
def<"active" | "inactive" | "pending">("status", () => "active");
// Function types
def<(x: number) => number>("double", () => (x) => x * 2);
// Promise types
def<Promise<User>>("asyncUser", async () => {
return await fetchUser();
});Type Safety Benefits
- Compile-time type checking: Catch type errors before runtime
- IntelliSense support: Get autocomplete and inline documentation
- Refactoring safety: TypeScript will flag issues when types change
- Self-documenting code: Types serve as inline documentation
Complete Example with Type Safety
import { describe, it, expect } from "bun:test";
import { def, get, subject } from "bdd-lazy-var-next/bun";
interface Product {
id: number;
name: string;
price: number;
}
describe("Shopping Cart", () => {
def("products", () => [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 25 },
]);
def("quantities", () => [1, 2]);
subject("totalPrice", () => {
// Use explicit type parameters for type safety
const products = get<Product[]>("products");
const quantities = get<number[]>("quantities");
return products.reduce((total, product, index) => {
return total + product.price * quantities[index];
}, 0);
});
it("calculates total price correctly", () => {
const total = subject<number>();
expect(total).toBe(1049); // 999*1 + 25*2
});
it("has type-safe property access", () => {
const products = get<Product[]>("products");
// TypeScript knows products is Product[]
expect(products[0].name).toBe("Laptop"); // ✓ Type-safe!
expect(products[0].price).toBe(999); // ✓ Autocomplete works!
expect(products.length).toBe(2);
});
it("alternative: using variable type annotations", () => {
// You can also use type annotations instead of explicit parameters
const products: Product[] = get("products");
const quantities: number[] = get("quantities");
expect(products.length).toBe(2);
expect(quantities.length).toBe(2);
});
});React Testing Library Example
bdd-lazy-var-next works perfectly with React Testing Library for component testing:
import { describe, it, expect, mock } from "bun:test";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { def, get, subject } from "bdd-lazy-var-next/bun";
import { UserProfile, type User } from "./UserProfile";
describe("UserProfile Component", () => {
// Define user with type safety
def("user", () => ({
id: get<number>("userId"),
name: get<string>("userName"),
email: get<string>("userEmail"),
role: get<"admin" | "user">("userRole"),
}));
def("userId", () => 1);
def("userName", () => "John Doe");
def("userEmail", () => "[email protected]");
def("userRole", () => "user" as const);
def("onEdit", () => mock(() => {}));
def("onDelete", () => mock(() => {}));
// Subject: render the component
subject("profile", () =>
render(
<UserProfile
user={get<User>("user")}
onEdit={get("onEdit")}
onDelete={get("onDelete")}
/>
)
);
it("renders user information", () => {
subject();
expect(screen.getByTestId("user-name")).toHaveTextContent("John Doe");
expect(screen.getByTestId("user-email")).toHaveTextContent(
"[email protected]"
);
});
it("calls onEdit when clicked", async () => {
subject();
await userEvent.click(screen.getByTestId("edit-button"));
expect(get("onEdit")).toHaveBeenCalledTimes(1);
});
describe("admin user", () => {
def("userName", () => "Admin User");
def("userRole", () => "admin" as const);
it("displays admin role", () => {
subject();
expect(screen.getByTestId("user-role")).toHaveTextContent("Role: admin");
});
});
});import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { def, get, subject } from "bdd-lazy-var-next/vitest";
import { Counter } from "./Counter";
describe("Counter Component", () => {
def("counterProps", () => ({
initialCount: get<number>("initialCount"),
label: get<string>("label"),
}));
def("initialCount", () => 0);
def("label", () => "Count");
subject("counter", () => render(<Counter {...get("counterProps")} />));
it("renders with default count", () => {
subject();
expect(screen.getByTestId("count-label")).toHaveTextContent("Count: 0");
});
it("increments the counter", async () => {
subject();
await userEvent.click(screen.getByTestId("increment"));
expect(screen.getByTestId("count-label")).toHaveTextContent("Count: 1");
});
describe("with custom initial count", () => {
def("initialCount", () => 10);
it("starts at the custom count", () => {
subject();
expect(screen.getByTestId("count-label")).toHaveTextContent("Count: 10");
});
});
});Key Benefits for React Testing:
- DRY Component Setup: Define props once, reuse in nested contexts
- Easy Overrides: Override specific props in nested describe blocks
- Type Safety: Full TypeScript support with React components
- Clean Tests: Focus on behavior, not setup boilerplate
- Lazy Rendering: Components only render when
subject()is called
Important: Prevent Memory Leaks
When using React Testing Library with bdd-lazy-var-next, you must add cleanup to prevent memory leaks:
For Bun:
// setup.ts
import { cleanup } from "@testing-library/react";
import { afterEach } from "bun:test";
afterEach(() => {
cleanup();
});For Vitest:
// setup.ts
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(() => {
cleanup();
});Why this matters:
Without cleanup, rendered components accumulate in memory across tests, causing:
- 🐌 Slow test execution (especially noticeable in Vitest)
- 💾 Memory leaks from accumulated DOM nodes and React instances
- ❌ Test interference from leftover state
The memory leak is particularly bad when using subject() with render() because each test renders a component that stays mounted unless explicitly cleaned up.
Bun Advanced Usage & Troubleshooting
Local Development & Linking
When testing changes in a local consumer project (e.g., examples/bun-consumer), you may want to link the package locally:
// package.json
"dependencies": {
"bdd-lazy-var-next": "file:../../"
}Note: Linking with file:../../ will copy the entire repo, including node_modules, which can be slow. For faster linking, use a minimal package or run npm pack/bun pack in the main repo and link the resulting .tgz file:
cd /Users/sujeetkc1/Desktop/bdd-lazy-var-next
bun run build
bun pack # or npm pack
# Then in consumer project:
bun add ../bdd-lazy-var-next/bdd-lazy-var-next-x.y.z.tgzPreload Setup for Bun
To register globals for all tests, use Bun's preload feature in bunfig.toml:
[test]
preload = ["./setup.ts"]// setup.ts
import "bdd-lazy-var-next/bun";Troubleshooting
- Double Initialization Error: If you see errors about variables being defined twice, ensure you are not importing the library globally in multiple places, and use unique variable names per test file.
- Local Linking Slow: Use a packed
.tgzfile for local development to avoid copying the entire repo. - TypeScript Types: Ensure your
tsconfig.jsonincludes the correct type paths for Bun and the library.
Motivation: Why the new way rocks
No more global leaks
Because lazy vars are cleared after each test, we didn't have to worry about test pollution anymore. This helped ensure isolation between our tests, making them a lot more reliable.
Clear meaning
Every time I see a get('<variable>') reference in my tests, I know where it's defined. That, coupled with removing exhaustive var declarations in describe blocks, have made even my largest tests clear and understandable.
The old way (for comparison)
describe("Suite", function () {
var name;
beforeEach(function () {
name = getName();
});
afterEach(function () {
name = null;
});
it("uses name variable", function () {
expect(name).to.exist;
});
});This pattern becomes difficult as tests grow, leading to "variable soup" and potential leaks.
Want to help?
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for contributing
