Use this when you have a user story with Gherkin acceptance criteria and need to generate failing tests before writing implementation code. Maps Given/When/Then directly to test setup/action/assertion. This is the first step in the TDD++ workflow — tests come before code.
Full guide: See the TDD guide for additional workflows (ping-pong, refactoring, pinning tests) and the TDD guide for the full practice.
Process
Step 1: Gather inputs
Ask the user:
- The user story — paste the full story with Gherkin acceptance criteria (Given/When/Then scenarios)
- Tech stack — language, test framework, and relevant libraries (e.g., TypeScript + Jest + React Testing Library, Python + pytest, Ruby + RSpec)
- Existing code context — (optional) relevant existing code, module structure, or API contracts the tests should work against
If the story doesn't have Gherkin acceptance criteria, ask the user to provide them or offer to help write them first (see /story-write).
Step 2: Map criteria to tests
For each acceptance criterion in the story, generate a failing test:
- Given → test setup (arrange)
- When → action (act)
- Then → assertion (assert)
- And/But → additional setup, actions, or assertions within the same test
Output the tests as a complete, runnable test file:
// Example structure (adapt to the user's tech stack)
describe('(Feature name)', () => {
// Scenario 1: (Scenario name from acceptance criteria)
it('should (expected behavior from Then clause)', () => {
// Given: (setup from Given clause)
// When: (action from When clause)
// Then: (assertion from Then clause)
});
// Scenario 2: (Error path scenario name)
it('should (error handling behavior)', () => {
// Given: (error setup)
// When: (action that triggers error)
// Then: (error assertion)
});
// Scenario 3: (Edge case scenario name)
it('should (edge case behavior)', () => {
// Given: (edge case setup)
// When: (action)
// Then: (expected behavior)
});
});
Step 3: Apply test design techniques
After the initial AC-to-test mapping, expand coverage using formal test design techniques:
For each input in the AC:
- Boundary value analysis (BVA): Test at min, max, min-1, max+1, zero, negative for numeric inputs
- Equivalence class partitioning (ECP): Test one value from each valid and invalid partition
- Decision tables: For features with multiple interacting conditions, test key combinations
Add expanded tests and mark them as // [BVA], // [ECP], or // [DT] to trace which technique generated them.
Related skills: For a more thorough expansion, use
/test-case-designbefore generating tests.
Step 4: Check for missing coverage
After generating tests for all acceptance criteria, review for gaps:
- Are there edge cases not covered by the acceptance criteria? (Empty states, max lengths, concurrent actions, invalid inputs)
- Are error paths covered? (Network failures, auth failures, validation errors)
- Are boundary conditions tested? (First item, last item, zero, one, many)
Propose additional tests for any gaps found. Mark them clearly as "additional coverage beyond AC."
Step 5: Review with the user
Present the test file and ask:
- Do these tests capture what the acceptance criteria intended?
- Are the test names clear enough that a failing test tells you exactly what's broken?
- Is the setup realistic for your codebase? (Mocking strategy, test data, dependencies)
- Any tests that seem redundant or that test the wrong thing?
Make edits as needed.
Related skills
/story-write— if the story doesn't have Gherkin acceptance criteria yet, write them first.
Output location
Present the test file as a code block in the conversation. The user copies it into their codebase and runs it to confirm all tests fail (Red phase of TDD).
Example Output
Input
- User story: As a customer, I want to apply a promo code at checkout so that I receive a discount on my order. Acceptance Criteria: Scenario 1 (Valid code): Given a cart totaling $120 and a valid promo code "SAVE20" offering 20% off, When the customer applies the code, Then the order total should be $96 and a success message "Promo code applied" should display. Scenario 2 (Expired code): Given a cart totaling $120 and an expired promo code "SUMMER23", When the customer applies the code, Then the total should remain $120 and an error "Promo code expired" should display. Scenario 3 (Invalid code): Given any cart and an unrecognized code "FAKECODE", When the customer applies the code, Then an error "Invalid promo code" should display.
- Tech stack: TypeScript + Jest + React Testing Library
- Existing code context:
applyPromoCode(code: string, cartTotal: number): Promise<{ total: number; message: string; error?: string }>lives insrc/checkout/promoService.ts; the UI component issrc/checkout/PromoCodeInput.tsx
Output
TDD Tests: Promo Code at Checkout
All tests below are written to fail until the implementation exists. Copy into your repo and confirm red before writing code.
// src/checkout/__tests__/promoCode.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { applyPromoCode } from '../promoService';
import { PromoCodeInput } from '../PromoCodeInput';
jest.mock('../promoService');
const mockApplyPromoCode = applyPromoCode as jest.MockedFunction<typeof applyPromoCode>;
describe('Promo Code at Checkout', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// ─── Acceptance Criteria Tests ───────────────────────────────────────────
// Scenario 1: Valid promo code applies discount
it('should reduce order total and show success message when a valid code is applied', async () => {
// Given: a cart totaling $120 and valid code "SAVE20" (20% off)
mockApplyPromoCode.mockResolvedValue({ total: 96, message: 'Promo code applied' });
render(<PromoCodeInput cartTotal={120} />);
// When: the customer enters and submits the promo code
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'SAVE20');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
// Then: total updates to $96 and success message is shown
await waitFor(() => {
expect(screen.getByText('$96.00')).toBeInTheDocument();
expect(screen.getByText('Promo code applied')).toBeInTheDocument();
});
expect(mockApplyPromoCode).toHaveBeenCalledWith('SAVE20', 120);
});
// Scenario 2: Expired promo code shows error, total unchanged
it('should keep original total and show expiry error when an expired code is applied', async () => {
// Given: a cart totaling $120 and expired code "SUMMER23"
mockApplyPromoCode.mockResolvedValue({
total: 120,
message: '',
error: 'Promo code expired',
});
render(<PromoCodeInput cartTotal={120} />);
// When: the customer applies the expired code
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'SUMMER23');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
// Then: total stays at $120 and error is displayed
await waitFor(() => {
expect(screen.getByText('$120.00')).toBeInTheDocument();
expect(screen.getByText('Promo code expired')).toBeInTheDocument();
});
});
// Scenario 3: Unrecognized promo code shows error
it('should show invalid code error when an unrecognized code is applied', async () => {
// Given: any cart and an unrecognized code "FAKECODE"
mockApplyPromoCode.mockResolvedValue({
total: 80,
message: '',
error: 'Invalid promo code',
});
render(<PromoCodeInput cartTotal={80} />);
// When: the customer applies the fake code
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'FAKECODE');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
// Then: error message is displayed
await waitFor(() => {
expect(screen.getByText('Invalid promo code')).toBeInTheDocument();
});
});
// ─── Boundary Value Analysis [BVA] ───────────────────────────────────────
// [BVA] Cart total at exactly $0
it('should handle a $0 cart total without crashing', async () => {
mockApplyPromoCode.mockResolvedValue({ total: 0, message: 'Promo code applied' });
render(<PromoCodeInput cartTotal={0} />);
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'SAVE20');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => expect(screen.getByText('$0.00')).toBeInTheDocument());
});
// [BVA] Cart total of $0.01 (minimum non-zero order)
it('should apply a discount to a $0.01 cart total', async () => {
mockApplyPromoCode.mockResolvedValue({ total: 0.01, message: 'Promo code applied' });
render(<PromoCodeInput cartTotal={0.01} />);
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'SAVE20');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => expect(screen.getByText('$0.01')).toBeInTheDocument());
});
// [BVA] Very large cart total ($99,999.99)
it('should correctly apply a discount to a large cart total', async () => {
mockApplyPromoCode.mockResolvedValue({ total: 79999.99, message: 'Promo code applied' });
render(<PromoCodeInput cartTotal={99999.99} />);
await userEvent.type(screen.getByRole('textbox', { name: /promo code/i }), 'SAVE20');
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => expect(screen.getByText('$79,999.99')).toBeInTheDocument());
});
// ─── Equivalence Class Partitioning [ECP] ────────────────────────────────
// [ECP] Empty promo code string (invalid input class)
it('should not call the service and should show a validation error when code is empty', async () => {
render(<PromoCodeInput cartTotal={120} />);
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
expect(screen.getByText('Please enter a promo code')).toBeInTheDocument();
expect(mockApplyPromoCode).not.toHaveBeenCalled();
});
// [ECP] Code with only whitespace (invalid input class)
it('should treat a whitespace-only code as empty and show a validation error', async () => {
render(<Pro