vitest-story
v0.0.3
Published
A lightweight, fast, and modern BDD framework built directly on top of Vitest.
Readme
vitest-story
A lightweight, fast, and modern BDD framework built directly on top of Vitest.
The Story Behind vitest-story
We built vitest-story out of frustration. While we appreciated the Behavior Driven Development (BDD) methodology, the existing tooling in the JavaScript ecosystem felt heavy and fragmented. Cucumber JS, while the standard, often introduced significant overhead, slow startup times, and a disconnect between the feature files and the code.
We wanted a solution that:
- Ran at the speed of modern development. We didn't want to wait for a slow test runner.
- Reduced context switching. We wanted our stories to live close to our code, not isolated in separate text files that drift out of sync.
- Leveraged the tools we already use. We didn't want a separate CLI or a complex configuration just to run BDD tests.
We chose Vitest as our bedrock because it is blazing fast, has excellent TypeScript support out of the box, and integrates seamlessly with the modern web ecosystem (Vite, Vue, React, etc.). vitest-story is a thin, powerful layer that brings Gherkin-style syntax directly into your Vitest suites.
Features
- Inline Stories: Write your Gherkin scenarios directly in your TypeScript/JavaScript test files using the
storytemplate literal. No more context switching between.featurefiles and step definitions. - Native Vitest Integration: Runs as standard Vitest tests. You get all the benefits of Vitest: watch mode, smart filtering, parallel execution, and instant feedback.
- Type-Safe Steps: Define steps (
Given,When,Then) with full TypeScript support. - Flexible Organization: Colocate steps with tests or organize them in a dedicated directory—it's up to you.
- Lifecycle Hooks: Full support for
beforeFeature,afterFeature,beforeScenario, andafterScenariohooks. - Zero Boilerplate: Minimal configuration required. Install, import, and write your first story.
Comparison with Cucumber JS
| Feature | Cucumber JS | vitest-story |
| :------------------- | :----------------------------- | :---------------------------------- |
| Execution Engine | Custom CLI / Runner | Vitest |
| Speed | Slower startup, overhead | Blazing fast (powered by Vite) |
| Syntax | .feature files (Gherkin) | Template literals in .ts/.js |
| Step Definitions | Separate files, regex matching | Imported functions, string matching |
| TypeScript | Requires setup/compilation | Native support |
| Ecosystem | Large, but fragmented | Integrated with Vite/Vitest |
Benefits
- Speed: Tests run instantly.
- Simplicity: No complex glue code. Just imports.
- Developer Experience: IntelliSense, type checking, and debugging work out of the box in your editor.
- Maintainability: Keep your requirements (stories) and verification (tests) in sync effortlessly.
Getting Started
Installation
npm install -D vitest vitest-story
# or
pnpm add -D vitest vitest-story
# or
yarn add -D vitest vitest-storyWriting Your First Story
- Define your steps. You can do this in a separate file or right next to your test.
// steps.ts
import { Given, When, Then } from "vitest-story";
import { expect } from "vitest";
let accountBalance = 0;
Given("my account balance is {int}", (amount: number) => {
accountBalance = amount;
});
When("I withdraw {int}", (amount: number) => {
accountBalance -= amount;
});
Then("my account balance should be {int}", (amount: number) => {
expect(accountBalance).toBe(amount);
});- Write your story. Create a test file (e.g.,
bank.test.ts).
// bank.test.ts
import { story } from "vitest-story";
import "./steps"; // Import your steps
story`
Feature: Bank Account Operations
Scenario: Successful Withdrawal
Given my account balance is 100
When I withdraw 20
Then my account balance should be 80
Scenario: Multiple Withdrawals
Given my account balance is 500
When I withdraw 50
And I withdraw 50
Then my account balance should be 400
`;- Run it.
npx vitestExample: Shopping Cart
Here is a more complete example showing how vitest-story handles data and multiple steps.
import { story } from "vitest-story";
import { expect } from "vitest";
import { Given, When, Then } from "vitest-story";
// Simple cart implementation for the example
const cart = {
items: [] as string[],
add(item: string) {
this.items.push(item);
},
contains(item: string) {
return this.items.includes(item);
},
size() {
return this.items.length;
},
};
Given("the shopping cart is empty", () => {
cart.items = [];
});
When("I add {string} to the cart", (item: string) => {
cart.add(item);
});
Then("the cart should contain {int} item(s)", (count: number) => {
expect(cart.size()).toBe(count);
});
Then("the cart should contain {string}", (item: string) => {
expect(cart.contains(item)).toBe(true);
});
story`
Feature: Shopping Cart
Scenario: Adding items
Given the shopping cart is empty
When I add "Apple" to the cart
And I add "Banana" to the cart
Then the cart should contain 2 items
And the cart should contain "Apple"
`;Tags
Tags allow you to organize and filter your scenarios. You can use tags to run subsets of your tests or skip specific scenarios.
Basic Tag Usage
Tags are prefixed with @ and can be placed before Feature or Scenario keywords:
import { story } from "vitest-story";
story`
@fast
@calculator
Feature: Calculator Operations
Background:
Given the calculator is reset
@smoke
Scenario: Addition
When I add 5
Then the result should be 5
@slow
Scenario: Complex calculation
When I perform complex operations
Then the result should be correct
`;Skipping Scenarios
Use the @skip tag to skip scenarios:
story`
Feature: Calculator
@skip
Scenario: This test is temporarily disabled
Given I have an incomplete feature
Then it should not run
`;Filtering Tests by Tags
You can filter tests by tags using the VITEST_STORY_TAGS environment variable:
# Run only scenarios tagged with @smoke
VITEST_STORY_TAGS=smoke npx vitest
# Run scenarios with either @smoke or @fast tags
VITEST_STORY_TAGS=smoke,fast npx vitestAlternatively, configure tags programmatically:
import { configureVitestStory } from "vitest-story";
configureVitestStory({
tags: ["smoke", "fast"],
});You can also use Vitest projects to configure different tag combinations in your vitest.config.ts:
import { defineConfig } from "vitest/config";
import { vitestStoryPlugin } from "vitest-story";
export default defineConfig({
plugins: [
vitestStoryPlugin({
stepsPaths: ["./steps"],
storyPaths: ["./stories"],
}),
],
test: {
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "**/*.story"],
// Define multiple projects with different tag filters
projects: [
{
extends: true,
test: {
name: "smoke-tests",
env: {
VITEST_STORY_TAGS: "smoke",
},
},
},
{
extends: true,
test: {
name: "fast-tests",
env: {
VITEST_STORY_TAGS: "smoke,fast",
},
},
},
],
},
});Run specific project:
# Run smoke tests only
npx vitest --project=smoke-tests
# Run fast tests
npx vitest --project=fast-tests
# Run all projects
npx vitestTag Inheritance
Scenarios inherit tags from their feature. If a feature is tagged with @fast, all scenarios in that feature are considered to have the @fast tag.
story`
@fast
Feature: Quick Tests
Scenario: Test 1
# This scenario inherits @fast from the feature
Given I start
`;Tag Priority
The @skip tag always takes priority. Even if a scenario matches your tag filter, it will be skipped if it has the @skip tag.
story`
Feature: Tests
@smoke @skip
Scenario: Skipped smoke test
# This will be skipped despite having @smoke tag
Given I start
`;