Module 8: Capstone Project — A Complete Test Suite with TDD
Spec-First Planning: Designing from the Tests
Spec-First Planning: Designing from the Tests
Capsule overview
Before writing a single line of implementation, you're going to design the TaskFlow API from the tests. This capsule is pure planning: decomposing features into user stories, mapping each story to testable specs, defining the exact names of the tests you'll write, and creating the empty file structure.
You don't implement anything here. You design the contract between what the application must do and how you'll validate it. By the end, you'll have a complete blueprint that guides all the subsequent development.
Why Planning Before Code?
The problem with "just start coding"
When you start implementing without planning, you tend to:
- Write features that don't cover all the necessary cases
- Discover requirements halfway through
- End up with tests that don't reflect the expected behavior
- Waste time refactoring because the architecture didn't anticipate certain cases
The spec-first approach
In spec-first, you define the tests as a specification before implementing:
- The tests are the contract: "the app must do X, Y, Z"
- Claude Code implements against that contract
- There's no ambiguity: the test passes or fails
- Feature decomposition forces you to think about edge cases before writing code
Feature Decomposition: Breaking Features into Specs
What feature decomposition is
Feature decomposition is splitting a large feature ("authentication") into individual, testable specs. Each spec is a test with a name that describes exactly what must happen.
An example:
A large feature: "Authentication"
→ Spec 1: register with a valid email creates a user
→ Spec 2: register with a duplicate email fails
→ Spec 3: login with correct credentials returns a token
→ Spec 4: login with an incorrect password fails
...
How to decompose
For each feature, ask yourself:
- What's the happy path?
- What errors can occur?
- What edge cases exist?
- What validations must apply?
Each answer becomes the name of a test.
User Stories → Test Specs: The Mapping
Feature 1: Authentication
User stories:
- As a user, I want to register with an email and password so I have an account
- As a user, I want to log in with my credentials to get a token
- As a user, I want my token to be valid so I can access protected endpoints
- As a user, I want invalid credentials to be rejected
Test specs (8-10 tests):
| # | Test name | What it validates |
|---|---|---|
| 1 | test_register_creates_user_with_valid_email_and_password | A successful registration creates a user and returns the data (without the password) |
| 2 | test_register_duplicate_email_fails | An already-registered email → ValueError |
| 3 | test_register_invalid_email_raises | A malformed email → ValueError |
| 4 | test_register_weak_password_raises | A password that doesn't meet the requirements → ValueError |
| 5 | test_login_returns_token_for_valid_credentials | A successful login returns a token string |
| 6 | test_login_wrong_password_raises | An incorrect password → ValueError |
| 7 | test_login_nonexistent_user_raises | The user doesn't exist → ValueError |
| 8 | test_validate_token_returns_user_for_valid_token | A valid token returns the user's data |
| 9 | test_validate_token_raises_for_invalid_token | An invalid or expired token → ValueError |
| 10 | test_password_is_hashed_on_register | The password is never stored in plain text |
Feature 2: Teams
User stories:
- As a user, I want to create a team to organize tasks with others
- As an owner, I want to add members to my team
- As a user, I want to list the teams I belong to
Test specs (6-8 tests):
| # | Test name | What it validates |
|---|---|---|
| 1 | test_create_team_returns_team_with_owner | Creating a team returns the team with the creator as owner |
| 2 | test_create_team_requires_authenticated_user | Without a token → 401 |
| 3 | test_add_member_adds_user_to_team | Adding a member successfully |
| 4 | test_add_member_requires_owner_permission | Only the owner can add members |
| 5 | test_add_member_duplicate_raises | Adding an already-existing member → error |
| 6 | test_list_teams_returns_user_teams | Listing teams returns only the user's |
| 7 | test_list_team_members_returns_correct_users | Listing members returns the owner + members |
| 8 | test_get_team_by_id_returns_team_or_404 | Getting a team by ID, 404 if it doesn't exist |
Feature 3: Tasks
User stories:
- As a user, I want to create tasks within a team
- As a user, I want to assign tasks to team members
- As a user, I want to change tasks' status (pending → in progress → completed)
- As a user, I want to filter tasks by status, priority and assignee
Test specs (10-12 tests):
| # | Test name | What it validates |
|---|---|---|
| 1 | test_create_task_returns_task_in_team | Creating a task returns the task with the correct data |
| 2 | test_create_task_requires_team_membership | A non-member user can't create a task |
| 3 | test_get_task_by_id_returns_task_or_404 | Getting a task by ID |
| 4 | test_update_task_modifies_fields | Updating the title, description, etc. |
| 5 | test_delete_task_removes_from_team | Deleting a task removes it from the team |
| 6 | test_assign_task_to_team_member_succeeds | Assigning a task to a team member |
| 7 | test_assign_task_to_non_member_fails | Assigning to a non-member fails |
| 8 | test_transition_pending_to_in_progress_succeeds | A valid transition pending → in_progress |
| 9 | test_transition_in_progress_to_completed_succeeds | A valid transition in_progress → completed |
| 10 | test_filter_tasks_by_status_returns_matching | Filtering by status returns only the matching ones |
| 11 | test_filter_tasks_by_priority_returns_matching | Filtering by priority |
| 12 | test_filter_tasks_by_assignee_returns_matching | Filtering by assignee |
Feature 4: Business Logic
User stories:
- As the system, I must validate that status transitions are valid (no going backwards)
- As the system, I must validate permissions: only the assignee can complete a task
- As a user, I want to see the team's statistics (tasks by status, completion rate)
Test specs (6-8 tests):
| # | Test name | What it validates |
|---|---|---|
| 1 | test_transition_completed_to_in_progress_raises | You can't go backwards completed → in_progress |
| 2 | test_transition_pending_to_completed_raises | You can't skip pending → completed (it must go through in_progress) |
| 3 | test_only_assigned_user_can_complete_task | Only the assignee can mark it as completed |
| 4 | test_team_owner_can_add_members | Owner permission to add |
| 5 | test_get_team_stats_returns_tasks_by_status | The stats return the count per status |
| 6 | test_get_team_stats_completion_rate | The completion rate is calculated correctly |
| 7 | test_unassigned_task_can_be_completed_by_any_member | An unassigned task: any member can complete it |
| 8 | test_invalid_status_transition_raises | A transition to an invalid status → ValueError |
Creating the Test Structure (Empty Files)
The directory structure
taskflow/
├── app/
│ └── ... (the app's structure)
├── tests/
│ ├── conftest.py
│ ├── unit/
│ │ ├── conftest.py
│ │ ├── test_auth.py
│ │ ├── test_teams.py
│ │ ├── test_tasks.py
│ │ └── test_rules.py
│ ├── integration/
│ │ ├── conftest.py
│ │ ├── test_auth_endpoints.py
│ │ ├── test_team_endpoints.py
│ │ └── test_task_endpoints.py
│ └── e2e/
│ ├── conftest.py
│ └── test_flows.py
File: tests/unit/test_auth.py (the empty structure)
# tests/unit/test_auth.py
# Tests for the auth service (registration, login, token validation)
def test_register_creates_user_with_valid_email_and_password():
pass
def test_register_duplicate_email_fails():
pass
def test_register_invalid_email_raises():
pass
def test_register_weak_password_raises():
pass
def test_login_returns_token_for_valid_credentials():
pass
def test_login_wrong_password_raises():
pass
def test_login_nonexistent_user_raises():
pass
def test_validate_token_returns_user_for_valid_token():
pass
def test_validate_token_raises_for_invalid_token():
pass
def test_password_is_hashed_on_register():
pass
File: tests/unit/test_teams.py
# tests/unit/test_teams.py
def test_create_team_returns_team_with_owner():
pass
def test_add_member_adds_user_to_team():
pass
def test_add_member_requires_owner_permission():
pass
def test_add_member_duplicate_raises():
pass
def test_list_teams_returns_user_teams():
pass
def test_list_team_members_returns_correct_users():
pass
def test_get_team_by_id_returns_team_or_404():
pass
File: tests/unit/test_tasks.py
# tests/unit/test_tasks.py
def test_create_task_returns_task_in_team():
pass
def test_create_task_requires_team_membership():
pass
def test_get_task_by_id_returns_task_or_404():
pass
def test_update_task_modifies_fields():
pass
def test_delete_task_removes_from_team():
pass
def test_assign_task_to_team_member_succeeds():
pass
def test_assign_task_to_non_member_fails():
pass
def test_filter_tasks_by_status_returns_matching():
pass
def test_filter_tasks_by_priority_returns_matching():
pass
def test_filter_tasks_by_assignee_returns_matching():
pass
File: tests/unit/test_rules.py
# tests/unit/test_rules.py
# Business logic: status transitions, permissions, stats
def test_transition_completed_to_in_progress_raises():
pass
def test_transition_pending_to_completed_raises():
pass
def test_only_assigned_user_can_complete_task():
pass
def test_get_team_stats_returns_tasks_by_status():
pass
def test_get_team_stats_completion_rate():
pass
def test_unassigned_task_can_be_completed_by_any_member():
pass
def test_invalid_status_transition_raises():
pass
The integration files (the structure)
For integration, the tests validate the endpoints with TestClient. The names reflect the HTTP behavior:
test_register_endpoint_returns_201_and_usertest_login_endpoint_returns_200_and_tokentest_protected_endpoint_returns_401_without_tokentest_create_team_endpoint_returns_201test_add_member_endpoint_returns_200test_create_task_endpoint_returns_201- etc.
(In the next capsule you'll implement these with real code.)
Prioritization: Core First, Business Logic Later
The recommended order
You don't implement all the tests in parallel. You follow an order that minimizes dependencies:
Phase 1 — Core (first):
- Auth (the whole module): without auth there are no users, without users there are no teams
- Teams (create, list): without teams there are no tasks
- Tasks (basic CRUD): create, read, update, delete
Phase 2 — Business logic:
- Status transitions
- Permissions (who can do what)
- Stats and filters
- Edge cases
Why this order
- Auth is the foundation: every protected endpoint depends on the token
- Teams is the second layer: tasks belong to teams
- Tasks CRUD is the functional core: without it the app doesn't do anything useful
- Business logic refines the behavior but presupposes that the CRUD exists
The Planning Checklist
Before moving on to the implementation, verify that you have:
- A complete list of tests per feature (auth, teams, tasks, business logic)
- Test files created with
passand descriptive names - The implementation order defined (auth → teams → tasks → rules)
- Clarity about which tests are unit vs integration vs E2E
Summary
- Feature decomposition turns "I want auth" into 10 concrete specs
- Each user story maps to tests with names that document the behavior
- You create the empty file structure before implementing
- You prioritize: core features (auth, teams, tasks CRUD) first; business logic later
- This capsule is only planning — in the next one the real TDD begins
Next capsule: Core Features with TDD — implementing auth, teams and tasks cycle by cycle.
Module 8, Capsule 02 — Testing with Claude Code Guide