CtrlK
BlogDocsLog inGet started
Tessl Logo

haletothewood/behavioural-tdd

Execute a strict Red-Green-Refactor TDD cycle — one requirement at a time — in any language or framework.

97

1.11x

Quality

100%

Does it follow best practices?

Impact

94%

1.11x

Average score across 5 eval scenarios

Overview
Skills
Evals
Files
name:
behavioural-tdd
description:
Execute a strict Red-Green-Refactor TDD cycle for a single requirement at a time, in any language or framework. Use when the user provides a business rule, acceptance criterion, or feature requirement and wants a failing test written first, followed by minimum implementation, then a clean refactor. Works for unit tests, integration tests, UI component tests, and API tests across stacks including TypeScript, JavaScript, React, .NET (C#/xUnit/NUnit), Python, Java, and others. Triggered by phrases like "write a test first", "TDD", "red-green-refactor", "behavioral test", "test-driven", "failing test first", or "write the test before the code".
license:
Apache-2.0
metadata:
{"author":"tessl-community","version":"1.8"}

Behavioral TDD (Red-Green-Refactor)

Execute one complete TDD cycle per invocation, in any language or framework. Never skip a phase. Never test private internals — only observable behavior through the public interface.


Phase 1 — RED: Write the Failing Test

Input: The requirement or business rule provided by the user.

  1. Identify the public entry point: a method, function, React component, HTTP endpoint, CLI command, or event handler. If it does not exist yet, define the ideal interface now.
  2. Write a single test that:
    • Calls only the public interface.
    • Asserts one specific output, return value, rendered element, or observable state change.
    • Will fail with a clear assertion error (not a compile/import error).
  3. Present the test with:
    • The test code block (in the user's language/framework).
    • One-line explanation of what behavior it proves.
    • The expected failure message or reason.

After presenting the test above, do not write any implementation code. Close your response and wait for the user to confirm before moving to Phase 2.


Phase 2 — GREEN: Minimum Implementation

Input: The failing test from Phase 1 (confirmed by the user).

  1. Write the smallest amount of code that makes the test pass.
    • "Shameless Green" is acceptable — hard-coding a return value to pass a single test is fine at this stage.
    • Do not add logic for requirements not yet tested.
  2. Present the implementation with:
    • The code block.
    • Confirmation the test would now pass.

After presenting the implementation above, close your response and wait for the user to confirm before moving to Phase 3.


Phase 3 — REFACTOR: Engineering Cleanup

Input: The passing code and behavioral test from Phases 1–2.

  1. Improve code quality without changing external behavior:
    • Rename variables/methods/components for clarity.
    • Extract magic values to named constants.
    • Remove duplication introduced by Shameless Green.
    • Apply idiomatic patterns for the chosen language/framework.
  2. State explicitly: "The behavioral test [name of test] still passes."
  3. Present the refactored code with:
    • A summary of what changed and why.

Mocking and Dependencies

Mock at system boundaries only — external APIs, databases, time, file system. Never mock your own classes, internal collaborators, or anything you control. Use dependency injection to make boundaries explicit and mockable.

AllowedNot Allowed
Mock an IEmailService injected via constructorMock a private _sendSmtp() method
Stub an HTTP client passed as a parameterAssert on internal variable values
Use a fake repository implementing a public interfaceSpy on private class internals
Intercept a React prop callbackAssert on React component internal state

Design interfaces that return results rather than produce side effects — functions that return values are easier to assert on than functions that mutate state. See mocking.md for patterns and examples.


Constraints

RuleRationale
Public interface onlyTests survive internal rewrites
One requirement per cycleFast feedback, clear failure signal
No mocking private methodsAvoids coupling tests to implementation
No assertions on internal statePreserves behavioral integrity
Shameless Green in Phase 2Establishes feedback loop before optimizing

UI Component Tests — Query Hierarchy

When writing tests for UI components, query by the highest-level user-facing role available. Prefer what a user or assistive technology sees over what the DOM implements.

PreferOverReason
getByRole('button', { name: /submit/i })getByTestId('submit-btn')Role + accessible name is user-facing
getByRole('textbox', { name: /email/i })getByPlaceholderText('email')Accessible name survives placeholder changes
getByRole('heading', { name: /confirm/i })querySelector('h2')Semantic role, not element type
getByRole('checkbox', { name: /agree/i })getByAttribute('type', 'checkbox')Role is stable across implementations

The name option on getByRole matches the accessible name (label text, aria-label, or aria-labelledby). Using it requires the component to expose a proper accessible name — which is itself a behavioral requirement worth enforcing in the test.

See tests.md for good vs bad test examples and red flags.


Examples by Stack

Python / pytest — Value calculation

Requirement: A ShoppingCart returns a total with 10% discount when 3+ items are added.

RED

def test_discount_applied_for_three_or_more_items():
    cart = ShoppingCart()
    cart.add_item(price=10.00)
    cart.add_item(price=10.00)
    cart.add_item(price=10.00)
    assert cart.total() == 27.00  # 10% off 30.00
# Fails: ShoppingCart is not defined

GREEN

class ShoppingCart:
    def __init__(self):
        self._items = []
    def add_item(self, price):
        self._items.append(price)
    def total(self):
        subtotal = sum(self._items)
        if len(self._items) >= 3:
            return subtotal * 0.90
        return subtotal

REFACTOR

The behavioral test test_discount_applied_for_three_or_more_items still passes.

BULK_DISCOUNT_RATE = 0.10
BULK_DISCOUNT_THRESHOLD = 3

class ShoppingCart:
    def __init__(self):
        self._items: list[float] = []

    def add_item(self, price: float) -> None:
        self._items.append(price)

    def total(self) -> float:
        subtotal = sum(self._items)
        if len(self._items) >= BULK_DISCOUNT_THRESHOLD:
            return subtotal * (1 - BULK_DISCOUNT_RATE)
        return subtotal

C# / xUnit — Domain service with injected dependency

Requirement: OrderService.PlaceOrder() returns OrderResult.Success when stock is available.

RED

[Fact]
public void PlaceOrder_ReturnsSuccess_WhenStockIsAvailable()
{
    var stockChecker = Substitute.For<IStockChecker>(); // injected dependency
    stockChecker.IsAvailable("SKU-001", 1).Returns(true);

    var service = new OrderService(stockChecker);
    var result = service.PlaceOrder("SKU-001", quantity: 1);

    Assert.Equal(OrderResult.Success, result);
}
// Fails: OrderService does not exist

GREEN

public class OrderService
{
    private readonly IStockChecker _stockChecker;
    public OrderService(IStockChecker stockChecker) => _stockChecker = stockChecker;

    public OrderResult PlaceOrder(string sku, int quantity)
    {
        if (_stockChecker.IsAvailable(sku, quantity))
            return OrderResult.Success;
        return OrderResult.OutOfStock;
    }
}

REFACTOR — no structural changes needed; code is already clear. Confirm: behavioral test still passes.


TypeScript / React Testing Library — UI component

Requirement: A SubmitButton renders as disabled when a loading prop is true.

RED

it('is disabled when loading', () => {
  render(<SubmitButton loading={true}>Save</SubmitButton>);
  expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
});
// Fails: SubmitButton is not defined

GREEN

export function SubmitButton({
  loading,
  children,
}: {
  loading: boolean;
  children: React.ReactNode;
}) {
  return <button disabled={loading}>{children}</button>;
}

REFACTOR

interface SubmitButtonProps {
  loading: boolean;
  children: React.ReactNode;
}

export function SubmitButton({ loading, children }: SubmitButtonProps) {
  return (
    <button disabled={loading} type="submit">
      {children}
    </button>
  );
}
// Behavioral test still passes.

Install with Tessl CLI

npx tessl i haletothewood/behavioural-tdd@1.8.0
Workspace
haletothewood
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
haletothewood/behavioural-tdd badge