Skip to main content

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.

Prerequisites

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​

AspectBDD/GherkinSpecification
Reader's focusFollowing a workflowInspecting exact rules and examples
VerbosityHigher (structured steps)Lower (direct code)
Best forWorkflows, acceptance testsDomain policies, contracts, edge cases
Data-drivenscenarioOutlineruleOutline
CollaborationDiscovery workshopsCode 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
caution

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 PatternPurposeExample
@smokeQuick sanity checks for CIscenario("Login @smoke", ...)
@slowTests over 10 secondsscenario("Bulk import @slow", ...)
@wipWork in progressfeature("New Feature @wip", ...)
@team-XTeam ownershipfeature("Payments @team-payments", ...)
@layerArchitecture layerfeature("API @api", ...)

Summary Checklist​

  • All test data appears in step titles (self-documenting)
  • Values extracted via ctx.step.values, ctx.step.params, or ctx.example
  • Optional feature and specification descriptions 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 / ruleOutline for data variations
  • background for shared setup steps
  • Consistent tagging convention
  • File names end in .Spec.ts
  • Then imported as uppercase, aliased to lowercase