Test driven development
Skill furkangonel/cowrangler/bundled_skills/software-development/test-driven-development
Autonomous terminal AI agent for workflows and feasible project procedures. Co-Worker Co-Wrangler π
npx -y skills add furkangonel/cowrangler --skill test-driven-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
TDD workflow β Red, Green, Refactor cycle with practical examples.
SKILL.md
10.1 KB, as published. Nobody here has run it
Test-Driven Development (TDD) SOP
Write a failing test, write just enough code to pass it, then refactor. Repeat. This SOP covers the full Red β Green β Refactor cycle with concrete examples.
When to Use
- User wants to build a feature with test coverage from the start
- User wants to practice TDD on an existing or new function
- User is adding behavior to an existing module and wants to do it safely
- User is fixing a bug and wants to prevent regression
The TDD Cycle
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β RED βββββββ GREEN βββββββ REFACTOR βββββββ RED β
β β
β Write a Make it pass Clean it up Next test β
β failing with minimal without (repeat) β
β test code breaking tests β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Non-negotiable rules:
- Never write production code before a failing test
- Write only enough production code to make the failing test pass
- Never refactor on a red test β only refactor when all tests are green
- Run tests after every change, no matter how small
Step 1 β RED: Write a Failing Test
Test Anatomy (Arrange β Act β Assert)
def test_calculate_order_total_applies_discount():
# Arrange β set up inputs
items = [
{"name": "Widget", "price": 10.00, "quantity": 3},
{"name": "Gadget", "price": 25.00, "quantity": 1},
]
discount_percent = 10
# Act β call the code under test
total = calculate_order_total(items, discount_percent)
# Assert β verify the result
assert total == 49.50 # (30 + 25) * 0.9
Good test names describe behavior, not implementation:
test_calculate_order_total_applies_discountβ goodtest_total_functionβ badtest_with_10_percent_off_correctly_applies_the_discount_to_three_itemsβ too verbose
Run It β Confirm It Fails
pytest tests/test_order.py::test_calculate_order_total_applies_discount -v
# Expected: FAILED (NameError or ImportError β function doesn't exist yet)
If the test passes immediately without writing production code, the test is wrong.
Step 2 β GREEN: Minimal Implementation
Write the simplest code that makes the test pass. Do not add features beyond what the test requires.
# order.py
def calculate_order_total(items, discount_percent=0):
subtotal = sum(item["price"] * item["quantity"] for item in items)
discount = subtotal * (discount_percent / 100)
return round(subtotal - discount, 2)
pytest tests/test_order.py::test_calculate_order_total_applies_discount -v
# Expected: PASSED β
Run the full test suite to ensure nothing regressed:
pytest tests/ -v
# All existing tests must still pass
Step 3 β REFACTOR: Clean Up
With green tests as a safety net, improve the code's structure without changing behavior.
# After refactor β same behavior, cleaner implementation
def calculate_order_total(items: list[dict], discount_percent: float = 0) -> float:
"""Calculate order total with optional percentage discount applied."""
if not 0 <= discount_percent <= 100:
raise ValueError(f"discount_percent must be 0-100, got {discount_percent}")
subtotal = sum(item["price"] * item["quantity"] for item in items)
return round(subtotal * (1 - discount_percent / 100), 2)
pytest tests/ -v
# All tests still green after refactor
Full TDD Walkthrough Example
Building a PasswordValidator class from scratch using TDD.
Iteration 1 β Minimum length
# tests/test_password_validator.py
from password_validator import PasswordValidator
def test_password_shorter_than_8_chars_is_invalid():
validator = PasswordValidator()
result = validator.validate("short")
assert result.is_valid is False
assert "at least 8 characters" in result.errors[0]
pytest -v # RED: ImportError
# password_validator.py β minimal implementation
from dataclasses import dataclass, field
@dataclass
class ValidationResult:
is_valid: bool
errors: list[str] = field(default_factory=list)
class PasswordValidator:
def validate(self, password: str) -> ValidationResult:
errors = []
if len(password) < 8:
errors.append("Password must be at least 8 characters")
return ValidationResult(is_valid=len(errors) == 0, errors=errors)
pytest -v # GREEN β
Iteration 2 β Must contain uppercase
def test_password_without_uppercase_is_invalid():
validator = PasswordValidator()
result = validator.validate("alllowercase1!")
assert result.is_valid is False
assert any("uppercase" in e for e in result.errors)
def test_valid_password_passes_all_checks():
validator = PasswordValidator()
result = validator.validate("SecurePass1!")
assert result.is_valid is True
assert result.errors == []
pytest -v # RED: test_password_without_uppercase_is_invalid fails
# Add to validate()
if not any(c.isupper() for c in password):
errors.append("Password must contain at least one uppercase letter")
pytest -v # GREEN β β both new tests pass, existing tests still pass
Iteration 3 β Refactor: extract validation rules
# Refactor: rules become composable β behavior unchanged
class PasswordValidator:
RULES = [
(lambda p: len(p) >= 8, "Password must be at least 8 characters"),
(lambda p: any(c.isupper() for c in p), "Password must contain at least one uppercase letter"),
(lambda p: any(c.isdigit() for c in p), "Password must contain at least one digit"),
(lambda p: any(c in "!@#$%^&*" for c in p), "Password must contain at least one special character"),
]
def validate(self, password: str) -> ValidationResult:
errors = [msg for check, msg in self.RULES if not check(password)]
return ValidationResult(is_valid=len(errors) == 0, errors=errors)
pytest -v # GREEN β β all tests pass, new rules covered by existing tests
What to Test vs What to Mock
Test directly:
- Pure functions (inputs β outputs, no side effects)
- Business logic and domain rules
- Data transformations and calculations
- Edge cases: empty input, max values, invalid types
Mock (replace with a fake):
- External API calls (HTTP requests, Stripe, Twilio)
- Database queries
- File system I/O
- Clock / time (
datetime.now()) - Random number generation
# Mocking an external API call
from unittest.mock import patch, MagicMock
def test_sends_welcome_email_after_registration():
user = {"email": "[email protected]", "name": "Alice"}
with patch("services.email.send_email") as mock_send:
mock_send.return_value = {"status": "sent"}
result = register_user(user)
assert result["success"] is True
mock_send.assert_called_once_with(
to="[email protected]",
subject="Welcome to our platform",
template="welcome",
)
TDD for Different Test Types
Unit Tests β single function or class
# Fast, no external deps, pure logic
def test_format_currency_rounds_to_two_decimals():
assert format_currency(1.2345) == "$1.23"
assert format_currency(0) == "$0.00"
assert format_currency(-5.5) == "-$5.50"
Integration Tests β multiple real components
# Slower, uses real DB/filesystem, tests component wiring
def test_user_registration_creates_db_record(test_db):
register_user({"email": "[email protected]", "name": "Alice"}, db=test_db)
user = test_db.query("SELECT * FROM users WHERE email = ?", "[email protected]")
assert user is not None
assert user["name"] == "Alice"
Run unit and integration tests separately:
pytest tests/unit/ -v # fast, run on every save
pytest tests/integration/ -v # slower, run before commit
Running Tests Efficiently
# Run a single test
pytest tests/test_order.py::test_calculate_order_total_applies_discount -v
# Run all tests in a file
pytest tests/test_order.py -v
# Run tests matching a keyword
pytest -k "discount" -v
# Stop on first failure
pytest -x -v
# Show coverage report
pytest --cov=src --cov-report=term-missing tests/
# Watch mode (install pytest-watch)
ptw tests/ -- -v
Common TDD Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Test passes without any production code | Test was already testing the wrong thing | Verify the test fails first, always |
| Too many things in one test | Hard to understand why it failed | One behavior per test |
| Testing implementation details | Test breaks on refactor even though behavior is correct | Test inputs/outputs, not internal method calls |
| Skipping the refactor step | Code gets messy over time | Refactor is not optional; budget time for it |
| Giant test setup | Every test has 50 lines of Arrange | Extract fixtures / factories |
| Mocking too much | Tests pass but production system is broken | Prefer integration tests for integration points |
TDD Checklist (per feature)
- Wrote a failing test first (confirmed it is red)
- Implemented only enough code to pass the test (no speculative features)
- All tests green after implementation
- Refactored code β tests still green
- Edge cases covered: empty input, null, boundary values
- External dependencies mocked in unit tests
- Coverage report checked for untested branches
- Tests are readable by a teammate unfamiliar with this code