Best Practices
Docs for Vitest SDK v0.3.3
This guide covers proven patterns for writing maintainable, self-documenting LiveDoc specs. Follow these practices to keep your test suite readable, reliable, and valuable as living documentation.
- A working LiveDoc Vitest setup (imports or globals)
- Familiarity with BDD and Specification patterns (Your First Feature, Your First Spec)
Embed All Values in Step Titles
This is the most important LiveDoc practice. Every input and expected output must appear in the step title — never hidden inside the implementation.
// ✅ BEST: Values visible in the step title, extracted via context
given("a user with balance '500' dollars", (ctx) => {
account.balance = ctx.step.values[0]; // 500
});
then("the balance should be '300' dollars", (ctx) => {
expect(account.balance).toBe(ctx.step.values[0]); // 300
});
// ✅ EVEN BETTER: Named parameters for clarity
given("a user with <balance:500> dollars", (ctx) => {
account.balance = ctx.step.params.balance; // 500
});
// ❌ BAD: Value drift — title says 500, code uses 200
given("a user with balance '500' dollars", (ctx) => {
account.balance = 200; // WRONG — doesn't match title
});
// ❌ WORSE: Hidden values — not living documentation
given("a user with some balance", () => {
account.balance = 500; // Reader can't see this in the report
});
Why this matters: LiveDoc produces human-readable reports. If values are hidden in code, readers see steps like "a user with some balance" — which tells them nothing. When values are in the title, the report becomes a complete specification.
Describe Why the Behavior Matters
Descriptions provide context that helps future readers (and your future self) understand why a feature or specification exists. Put the purpose in the optional container description and the proof in Scenario steps or Rule titles and assertions. A developer-facing Specification needs a contract purpose, not an invented business workflow or a list of tested branches:
feature(`Shopping Cart Checkout
@checkout @critical
Keeps cart totals and shipping tiers predictable for
the checkout cases covered below.
`, () => {
// scenarios...
});
specification(`Email Validation
@validation
Helps callers reject malformed addresses before
using them in the flows covered by these rules.
`, () => {
// rules...
});
Descriptions appear in LiveDoc reports and the Viewer UI. They are optional; avoid claiming delivery, compliance, or a UI outcome from an in-process validator or a response-only test.
Attach Safe API Evidence
For an HTTP contract, show the request and the selected actual response
fields on the Rule they support. Capture the evidence before a potentially
failing assertion; otherwise a failed test will skip later attachments.
This example expects API_BASE_URL to point to an isolated test server:
import { expect } from "vitest";
import { specification, rule } from "@swedevtools/livedoc-vitest";
specification(`Widget API
Clients can rely on a stable response when creating a widget.
`, () => {
rule("Creating widget 'sample' returns status '201' and name 'sample'", async (ctx) => {
const [name, expectedStatus, expectedName] = ctx.rule.values;
const baseUrl = process.env.API_BASE_URL;
if (!baseUrl) throw new Error("API_BASE_URL must point to an isolated test server");
const request = { name };
ctx.rule.attachJSON(request, "Create widget request (safe fields)");
const response = await fetch(`${baseUrl}/api/widgets`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
ctx.rule.attachJSON({ status: response.status }, "Create widget HTTP status");
const body: unknown = await response.json();
if (typeof body !== "object" || body === null ||
!("name" in body) || typeof body.name !== "string") {
throw new Error("Create widget response is missing a string name");
}
ctx.rule.attachJSON({ name: body.name }, "Widget response name (safe field)");
expect(response.status).toBe(expectedStatus);
expect(body.name).toBe(expectedName);
});
});
Only attach reviewed, non-sensitive request and response fields; never attach
raw headers, tokens, cookies, personal data, or an unreviewed response body.
Attachments are evidence, not assertions. For a browser flow, the
screenshot(page(), ctx) helper similarly documents
meaningful states or failures, but does not replace a behavioral assertion.
See the rule attachment reference
for the API and the static-export warning
before sharing reports.
Choose the Right Pattern
Use BDD/Gherkin When...
- The preconditions, action, and outcome tell a useful story
- You're testing a user or operator workflow
- A scenario makes the observable behavior clearer than a list of rules
feature("Account Withdrawal", () => {
scenario("Sufficient funds", () => {
given("an account with balance '$1000'", (ctx) => { /* ... */ });
when("the holder withdraws '$200'", (ctx) => { /* ... */ });
then("the balance is '$800'", (ctx) => { /* ... */ });
});
});
Use Specification/Rule When...
- You're documenting a precise business policy or technical contract
- You want compact, direct assertions
- Many data-driven variations are needed
- No Given/When/Then ceremony is needed
specification("URL Parser", () => {
rule("Extracts hostname from 'https://example.com/path'", (ctx) => {
const url = ctx.rule.values[0]; // "https://example.com/path"
expect(parseHostname(url)).toBe("example.com");
});
ruleOutline(`Handles various protocols
Examples:
| url | protocol |
| https://example.com | https |
| http://example.com | http |
| ftp://files.example.com | ftp |
`, (ctx) => {
expect(parseProtocol(ctx.example.url)).toBe(ctx.example.protocol);
});
});
Quick Decision Guide
| Aspect | BDD/Gherkin | Specification |
|---|---|---|
| Reader's focus | Following a workflow | Inspecting exact rules and examples |
| Verbosity | Higher (structured steps) | Lower (direct code) |
| Best for | Workflows, acceptance tests | Domain policies, contracts, edge cases |
| Data-driven | scenarioOutline | ruleOutline |
| Collaboration | Discovery workshops | Code reviews |
Tip: Mix patterns under the same capability. A product manager may read an Orders Checkout Feature and its Shipping rates Specification to inspect exact calculations. See Test Organization.
Organize Files by Domain
Structure your test files to mirror your business capabilities, not your source-code layers or test patterns. This creates a navigable hierarchy in the LiveDoc Viewer:
tests
├── Orders
│ ├── Checkout.Spec.ts Feature
│ └── Pricing
│ ├── ShippingRates.Spec.ts Specification
│ └── Discounts.Spec.ts Specification
└── Accounts
├── Registration.Spec.ts Feature
└── PasswordPolicy.Spec.ts Specification
Avoid a flat directory or separate Features/Orders and
Specifications/Orders roots. Both make it harder to find all the
documentation for Orders in one place.
One Concept Per Scenario
Each scenario should test a single behavior. If you find yourself adding
multiple when steps, consider splitting into separate scenarios:
// ❌ Too much in one scenario
scenario("Full checkout flow", () => {
given("items in the cart", () => { /* ... */ });
when("the user enters shipping info", () => { /* ... */ });
and("the user enters payment info", () => { /* ... */ });
and("the user confirms the order", () => { /* ... */ });
then("the order is placed", () => { /* ... */ });
and("the user receives a confirmation email", () => { /* ... */ });
and("inventory is updated", () => { /* ... */ });
});
// ✅ Focused scenarios
scenario("Place an order with valid payment", () => {
given("a cart with '2' items", (ctx) => { /* ... */ });
when("the user completes checkout", () => { /* ... */ });
then("the order status is 'confirmed'", (ctx) => { /* ... */ });
});
scenario("Order confirmation triggers email", () => {
given("a confirmed order for 'alice@example.com'", (ctx) => { /* ... */ });
when("the order is processed", () => { /* ... */ });
then("a confirmation email is sent to 'alice@example.com'", (ctx) => { /* ... */ });
});
Use Descriptive Names
Scenario names should describe the behavior, not the test:
// ❌ Vague names
scenario("Test case 1", () => { /* ... */ });
scenario("It works", () => { /* ... */ });
scenario("Edge case", () => { /* ... */ });
// ✅ Descriptive names
scenario("Applying a 20% discount reduces the total", () => { /* ... */ });
scenario("Expired coupon codes are rejected with an error", () => { /* ... */ });
scenario("Zero-quantity items are removed from the cart", () => { /* ... */ });
Use scenarioOutline for Data Variations
When testing the same behavior with different inputs, use scenarioOutline
instead of duplicating scenarios:
// ❌ Duplicated scenarios
scenario("Valid email: user@example.com", () => { /* ... */ });
scenario("Valid email: test@domain.org", () => { /* ... */ });
scenario("Invalid email: missing-at-sign", () => { /* ... */ });
// ✅ Data-driven with scenarioOutline
scenarioOutline(`Email validation
Examples:
| email | valid |
| user@example.com | true |
| test@domain.org | true |
| missing-at-sign | false |
| @no-local-part | false |
`, (ctx) => {
when("validating '<email>'", (ctx) => {
result = validateEmail(ctx.example.email);
});
then("the result is '<valid>'", (ctx) => {
expect(result).toBe(ctx.example.valid);
});
});
Use Background for Shared Setup
Extract common given steps into a background:
feature("Order Management", () => {
background("Authenticated user with items", () => {
given("a logged-in user 'Alice'", () => {
// setup auth
});
given("a cart with '3' items", (ctx) => {
// setup cart
});
});
scenario("View order summary", () => {
when("Alice views the order summary", () => { /* ... */ });
then("'3' items are displayed", (ctx) => { /* ... */ });
});
scenario("Apply discount code", () => {
when("Alice applies code 'SAVE10'", (ctx) => { /* ... */ });
then("the total is reduced by '10' percent", (ctx) => { /* ... */ });
});
});
Tag Strategically
Use a consistent tagging convention across your team:
| Tag Pattern | Purpose | Example |
|---|---|---|
@smoke | Quick sanity checks for CI | scenario("Login @smoke", ...) |
@slow | Tests over 10 seconds | scenario("Bulk import @slow", ...) |
@wip | Work in progress | feature("New Feature @wip", ...) |
@team-X | Team ownership | feature("Payments @team-payments", ...) |
@layer | Architecture layer | feature("API @api", ...) |
Summary Checklist
- All test data appears in step titles (self-documenting)
- Values extracted via
ctx.step.values,ctx.step.params, orctx.example - Optional
featureandspecificationdescriptions explain why the tested behavior matters - API evidence is allowlisted, attached before failing assertions, and supplements proof
- One concept per scenario
- Related Features and Specifications organized under the same capability
-
scenarioOutline/ruleOutlinefor data variations -
backgroundfor shared setup steps - Consistent tagging convention
- File names end in
.Spec.ts -
Thenimported as uppercase, aliased to lowercase
Related
- Setup: Explicit Imports — recommended import pattern
- Tags and Filtering — tag filtering configuration
- Data APIs — value extraction reference
- Your First Feature — BDD pattern tutorial
- Your First Spec — Specification pattern tutorial