Skip to content

Commit 5c8600d

Browse files
testac974claude
andcommitted
Complete P1-004: Architecture Principles chapter
Drafted remaining 4 sections (07-10) to complete the Architecture Principles chapter: - Section 07: Practical Application - Complete task management API example applying all 5 principles with Claude Code collaboration - Section 08: Common Pitfalls - 8 architecture anti-patterns with examples and solutions - Section 09: Summary - Synthesis of all 5 principles with quick reference checklist - Section 10: Further Reading - Curated resources including book references, tools, specs, and community links Chapter now complete with 10 sections, 8 Mermaid diagrams total. Updated tasks.md to mark P1-004 complete (11/81 tasks done). Fixed broken links in sections 09 and 10 (pointed to unwritten chapters). Note: Some markdown lint issues remain in section 06 (written earlier) that need to be addressed separately in a cleanup pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9567b99 commit 5c8600d

6 files changed

Lines changed: 1694 additions & 96 deletions

File tree

book/part1-foundations/03-architecture-principles/06-testability-for-ai-code.md

Lines changed: 344 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ part: 1
55
chapter: 3
66
section: 6
77
version: "0.1"
8-
date: "2026-01-28"
8+
date: "2026-01-29"
99
status: "draft"
1010
author: "Brian Childress"
1111
tags: ["architecture", "testing", "testability", "foundations"]
@@ -16,21 +16,351 @@ related:
1616
requirements:
1717
- REQ-C004
1818
abstract: |
19-
[Placeholder: 1-2 sentence summary of this section's content
20-
for search and navigation purposes. To be written during drafting.]
19+
Explains why testability is critical for agentic development and how to
20+
architect systems that enable fast, reliable testing of AI-generated code.
21+
Covers dependency injection, pure functions, and test boundaries.
2122
---
2223

23-
[Placeholder: Explain why testability is critical for agentic development.
24-
When AI generates code, you need fast, reliable tests to validate correctness.
25-
Architecture must enable easy testing - dependency injection, pure functions, mocked boundaries.
26-
Show examples of testable vs. untestable architectures and their impact on AI development velocity.
27-
~3-5 pages.]
24+
# Testability: Validating AI-Generated Code
2825

29-
**Testability principles**:
26+
When AI generates code 10x faster than humans, what becomes the bottleneck? Testing. You can ask Claude Code to build an entire REST API in 30 minutes, but how do you know it works correctly? How do you validate that the AI understood your requirements and implemented them faithfully?
3027

31-
- [Principle 1: Dependency injection for mocking]
32-
- [Principle 2: Pure functions and predictable behavior]
33-
- [Principle 3: Test boundaries aligned with component boundaries]
34-
- [Principle 4: Fast feedback loops]
28+
The answer isn't "test less." It's **architect for testability from the start**. When your system is designed to be easily tested, you can iterate with AI agents at high velocity while maintaining confidence that the code does what it's supposed to do.
3529

36-
[Additional subsections on testability patterns]
30+
## Why Testability is Critical for Agentic Development
31+
32+
In traditional development, you write code slowly and carefully, testing as you go. You intimately understand every line because you wrote it. With AI-generated code, you're reviewing hundreds or thousands of lines you didn't write. You need a different validation strategy.
33+
34+
Consider this scenario: You prompt Claude Code to implement user authentication. Five minutes later, you have a complete implementation with password hashing, session management, JWT tokens, and email verification. It looks sophisticated. But does it work? Are there security vulnerabilities? Does it handle edge cases?
35+
36+
Without good tests, you're forced to:
37+
- **Manually test every scenario** (slow and error-prone)
38+
- **Read and understand all the generated code** (defeats the velocity gains)
39+
- **Hope the AI got it right** (dangerous)
40+
41+
With a testable architecture, you can:
42+
- **Run automated tests in seconds** and know if it works
43+
- **Trust the AI-generated code** if tests pass
44+
- **Iterate quickly** by regenerating implementations and re-running tests
45+
46+
Testability transforms AI code generation from "risky and slow to validate" to "fast and verifiable."
47+
48+
## The Four Pillars of Testable Architecture
49+
50+
### 1. Dependency Injection: Control Your Collaborators
51+
52+
The most powerful testability pattern is **dependency injection**—providing dependencies from outside rather than creating them inside your code. This lets you swap real implementations with test doubles (mocks, stubs, fakes).
53+
54+
**Untestable approach:**
55+
56+
```python
57+
class UserService:
58+
def create_user(self, email, password):
59+
# Hard-coded dependency - can't test without real DB
60+
db = PostgresDatabase("production-db-url")
61+
hashed = bcrypt.hash(password)
62+
user_id = db.insert("users", {
63+
"email": email,
64+
"password_hash": hashed
65+
})
66+
67+
# Hard-coded email service - sends real emails in tests!
68+
email_service = SendGridEmailService()
69+
email_service.send_welcome_email(email)
70+
71+
return user_id
72+
```
73+
74+
This code is **impossible to test** without:
75+
- A real PostgreSQL database running
76+
- SendGrid credentials configured
77+
- Actually sending emails during tests
78+
79+
**Testable approach with dependency injection:**
80+
81+
```python
82+
class UserService:
83+
def __init__(self, database, email_service, hasher):
84+
self.database = database
85+
self.email_service = email_service
86+
self.hasher = hasher
87+
88+
def create_user(self, email, password):
89+
hashed = self.hasher.hash(password)
90+
user_id = self.database.insert("users", {
91+
"email": email,
92+
"password_hash": hashed
93+
})
94+
self.email_service.send_welcome_email(email)
95+
return user_id
96+
```
97+
98+
Now in tests, you can inject test doubles:
99+
100+
```python
101+
def test_create_user():
102+
# Arrange: Create test doubles
103+
fake_db = InMemoryDatabase()
104+
mock_email = MockEmailService()
105+
fake_hasher = FakeHasher()
106+
107+
service = UserService(fake_db, mock_email, fake_hasher)
108+
109+
# Act: Call the method
110+
user_id = service.create_user("test@example.com", "pass123")
111+
112+
# Assert: Verify behavior
113+
assert fake_db.contains("users", user_id)
114+
assert mock_email.was_called_with("test@example.com")
115+
assert fake_hasher.hash_called_once()
116+
```
117+
118+
The test runs in **milliseconds**, requires **no infrastructure**, and **verifies the logic** completely.
119+
120+
When you prompt an AI agent to implement a feature, you can specify: "Use dependency injection for all external services." The AI will generate testable code, and you can validate it immediately with tests.
121+
122+
### 2. Pure Functions: Predictable and Easy to Test
123+
124+
A **pure function** is one that:
125+
- **Always returns the same output for the same inputs** (deterministic)
126+
- **Has no side effects** (doesn't modify external state, doesn't do I/O)
127+
128+
Pure functions are the easiest code to test because you just call them with inputs and check outputs. No setup, no mocking, no cleanup.
129+
130+
**Impure function (hard to test):**
131+
132+
```python
133+
def calculate_total_price(cart_id):
134+
# Reaches into global state
135+
cart = GLOBAL_CART_STORE[cart_id]
136+
137+
# Makes database call
138+
tax_rate = database.get_tax_rate(cart.shipping_address)
139+
140+
# Uses current time (non-deterministic)
141+
if datetime.now().hour >= 18:
142+
# Evening discount
143+
discount = 0.1
144+
else:
145+
discount = 0.0
146+
147+
total = sum(item.price for item in cart.items)
148+
return total * (1 + tax_rate) * (1 - discount)
149+
```
150+
151+
This function is **painful to test**:
152+
- Need to set up global cart store
153+
- Need a database with tax rates
154+
- Behavior changes based on time of day
155+
- Hard to test edge cases
156+
157+
**Pure function (easy to test):**
158+
159+
```python
160+
def calculate_total_price(items, tax_rate, discount_rate):
161+
"""Calculate total price with tax and discount.
162+
163+
Args:
164+
items: List of items with prices
165+
tax_rate: Tax rate as decimal (e.g., 0.08 for 8%)
166+
discount_rate: Discount as decimal (e.g., 0.1 for 10%)
167+
168+
Returns:
169+
Total price with tax and discount applied
170+
"""
171+
subtotal = sum(item.price for item in items)
172+
with_tax = subtotal * (1 + tax_rate)
173+
with_discount = with_tax * (1 - discount_rate)
174+
return round(with_discount, 2)
175+
```
176+
177+
Now the test is trivial:
178+
179+
```python
180+
def test_calculate_total_price():
181+
items = [
182+
Item(price=10.00),
183+
Item(price=20.00)
184+
]
185+
186+
total = calculate_total_price(
187+
items=items,
188+
tax_rate=0.08,
189+
discount_rate=0.1
190+
)
191+
192+
# 30.00 + 8% tax = 32.40, - 10% discount = 29.16
193+
assert total == 29.16
194+
```
195+
196+
No setup, no mocking, just inputs and outputs. You can test dozens of scenarios in seconds.
197+
198+
**Architectural principle**: Push impure operations (I/O, state mutation, time) to the edges of your system. Keep the core logic pure. This makes the bulk of your code trivially testable.
199+
200+
### 3. Test Boundaries Aligned with Component Boundaries
201+
202+
Remember component decomposition from earlier sections? Well-designed component boundaries make natural test boundaries. Each component should be testable independently.
203+
204+
```mermaid
205+
graph TD
206+
A[Shopping Cart Feature] --> B[Cart Service]
207+
A --> C[Inventory Service]
208+
A --> D[Pricing Service]
209+
210+
B -.->|test boundary| B1[Cart Service Tests]
211+
C -.->|test boundary| C1[Inventory Service Tests]
212+
D -.->|test boundary| D1[Pricing Service Tests]
213+
214+
A -.->|integration test| A1[Cart Feature Integration Tests]
215+
216+
style B1 fill:#e1f5e1
217+
style C1 fill:#e1f5e1
218+
style D1 fill:#e1f5e1
219+
style A1 fill:#fff4e1
220+
```
221+
222+
*Figure 3.6: Component boundaries define test boundaries. Each component has focused unit tests, while integration tests verify components work together.*
223+
224+
When components have:
225+
- **Clear interfaces**: Easy to mock for testing
226+
- **Single responsibility**: Tests focus on one thing
227+
- **Explicit dependencies**: Easy to inject test doubles
228+
229+
Then you can:
230+
- **Test each component in isolation** (fast unit tests)
231+
- **Test integrations between components** (targeted integration tests)
232+
- **Know exactly what broke** when tests fail (precise test boundaries)
233+
234+
### 4. Fast Feedback Loops
235+
236+
The final pillar is **speed**. Tests must be fast enough to run constantly during development. If tests take 10 minutes to run, you won't run them frequently. If they take 10 seconds, you'll run them after every change.
237+
238+
**Slow tests kill agentic development velocity.** You iterate with AI, generate code, run tests, iterate again. This cycle needs to be measured in seconds, not minutes.
239+
240+
**Architecture for fast tests:**
241+
242+
```python
243+
# Bad: Slow integration test for every function
244+
def test_user_creation_full_stack():
245+
# Starts entire application (10 seconds)
246+
app = start_full_application()
247+
248+
# Makes real HTTP request (1 second)
249+
response = requests.post("http://localhost/api/users",
250+
json={"email": "test@example.com"})
251+
252+
# Queries real database (1 second)
253+
user = database.query("SELECT * FROM users WHERE email=?",
254+
"test@example.com")
255+
256+
assert user.email == "test@example.com"
257+
258+
# Total: ~12 seconds per test
259+
260+
261+
# Good: Fast unit test with mocks
262+
def test_user_creation_logic():
263+
# Creates lightweight objects (< 1ms)
264+
mock_db = MockDatabase()
265+
service = UserService(mock_db)
266+
267+
# Calls function directly (< 1ms)
268+
service.create_user("test@example.com", "password")
269+
270+
# Verifies mock was called correctly (< 1ms)
271+
assert mock_db.insert_was_called()
272+
273+
# Total: < 10ms per test
274+
```
275+
276+
With fast unit tests, you can run hundreds of tests in seconds. This enables:
277+
- **Continuous testing** while developing with AI
278+
- **Immediate feedback** when something breaks
279+
- **Confidence to iterate rapidly** knowing tests will catch regressions
280+
281+
## Testability Checklist for Agentic Development
282+
283+
When architecting a new system or feature, verify:
284+
285+
- [ ] **Dependencies are injected**, not hard-coded
286+
- [ ] **Core logic is pure functions** where possible
287+
- [ ] **I/O and state changes** are pushed to boundaries
288+
- [ ] **Components are independently testable**
289+
- [ ] **Interfaces are mockable** for testing
290+
- [ ] **Tests run in milliseconds**, not seconds or minutes
291+
- [ ] **Test coverage includes edge cases** and error conditions
292+
- [ ] **AI agents can generate tests** from specifications
293+
294+
When prompting AI to implement features, include testability requirements:
295+
296+
> "Implement user authentication with dependency injection for database and email services. Write pure functions for password hashing and validation. Include unit tests for all logic."
297+
298+
The AI will generate testable code, and you can validate it immediately.
299+
300+
## The Testability-Velocity Feedback Loop
301+
302+
Here's the beautiful thing about testable architecture: it creates a positive feedback loop with AI development velocity.
303+
304+
1. **Testable code → Fast test execution** (seconds, not minutes)
305+
2. **Fast tests → Frequent testing** (after every AI iteration)
306+
3. **Frequent testing → Quick error detection** (catch problems immediately)
307+
4. **Quick detection → Rapid iteration** (fix and regenerate quickly)
308+
5. **Rapid iteration → Higher velocity** (more features shipped)
309+
6. **Higher velocity → More need for testing** (more code to validate)
310+
7. **More testing → More testability needed** (architecture improves)
311+
312+
This loop accelerates over time. The more you optimize for testability, the faster you can safely iterate with AI agents.
313+
314+
## Common Testability Mistakes
315+
316+
**Mistake 1: Testing implementation details instead of behavior**
317+
318+
Don't test *how* the code works internally. Test *what* it does from the outside.
319+
320+
```python
321+
# Bad: Brittle test of implementation details
322+
def test_password_hashing_uses_bcrypt():
323+
service = UserService()
324+
# This breaks if you switch from bcrypt to argon2
325+
assert isinstance(service.hasher, BcryptHasher)
326+
327+
# Good: Tests the behavior contract
328+
def test_password_hashing_is_secure():
329+
service = UserService()
330+
password = "mysecret123"
331+
332+
# Hash is different from plaintext
333+
hashed = service.hash_password(password)
334+
assert hashed != password
335+
336+
# Same password verifies successfully
337+
assert service.verify_password(password, hashed)
338+
339+
# Different password fails
340+
assert not service.verify_password("wrong", hashed)
341+
```
342+
343+
**Mistake 2: Skipping tests because "AI code is usually correct"**
344+
345+
AI code is usually *plausible*, not necessarily correct. It can have subtle bugs, security issues, or misunderstand requirements. Tests catch these.
346+
347+
**Mistake 3: Over-mocking to the point of meaningless tests**
348+
349+
```python
350+
# Bad: Mocks so much that test is meaningless
351+
def test_create_user():
352+
mock_service = Mock()
353+
mock_service.create_user.return_value = 123
354+
355+
# This always passes - you're testing the mock!
356+
result = mock_service.create_user("email", "pass")
357+
assert result == 123
358+
```
359+
360+
Only mock external dependencies (databases, APIs, etc.). Test your own logic for real.
361+
362+
## What's Next
363+
364+
You now understand how to architect for testability—dependency injection, pure functions, aligned boundaries, and fast feedback. But testability alone isn't enough. You need to apply these principles systematically across your entire system.
365+
366+
In the next section, [Practical Application](./07-practical-application.md), we'll walk through architecting a complete system using all the principles from this chapter: digestibility, decomposition, interfaces, separation of concerns, and testability. You'll see how these principles work together in practice.

0 commit comments

Comments
 (0)