---
title: "The Feedback Loop Series Part 1: Test-Driven Development and the Foundation of Observable Engineering"
description: "TDD isn't just about testing—it's about creating fast, objective feedback loops that make software observable. Understanding this foundation is critical for modern development, from frontend tooling to AI agents."
date: 2025-12-17T00:00:00.000Z
author: "Kobi Kadosh"
url: https://kobi.kadosh.me/blog/feedback-loops-part-1-tdd
category: "Developer Philosophy"
tags: ["TDD", "Testing", "Software Engineering", "Series", "Career Advice"]
---

# The Feedback Loop Series Part 1: Test-Driven Development and the Foundation of Observable Engineering

_This is Part 1 of a 3-part series on feedback loops in software engineering. [Read the overview](https://kobi.kadosh.me/blog/feedback-loops-software-engineering) | [Part 2: UI Feedback Patterns](https://kobi.kadosh.me/blog/feedback-loops-part-2-ui) | [Part 3: AI Agent Loops](https://kobi.kadosh.me/blog/feedback-loops-part-3-agents)_

## The Misunderstood Practice

Test-driven development gets a bad rap. I've heard countless objections:

- "It slows me down"
- "It's dogmatic"
- "I don't have time to write tests first"
- "My code is too complex for TDD"
- "It only works for simple CRUD apps"

Here's the truth: **if you're arguing against TDD, you're probably misunderstanding what TDD actually is**.

TDD isn't about testing. It's about **creating a feedback loop that makes your software observable**.

## What TDD Actually Is

Strip away the ceremony, the dogma, the "rules"—and TDD is simply:

### 1. Externalize your expectation

```javascript

test('user can log in with valid credentials', () => {
  // This test IS your specification
  const user = { email: 'test@example.com', password: 'secure123' };
  const result = login(user);

  expect(result.success).toBe(true);
  expect(result.token).toBeDefined();
});
```

### 2. Observe it fail

```text

❌ user can log in with valid credentials
   ReferenceError: login is not defined
```

### 3. Make it pass (minimally)

```javascript

function login(user) {
  // Simplest thing that could possibly work
  return {
    success: true,
    token: 'mock-token'
  };
}
```

### 4. Refine and refactor

```javascript

function login(user) {
  // Now add real logic
  const validUser = validateCredentials(user);
  if (!validUser) return { success: false };

  const token = generateAuthToken(validUser);
  return { success: true, token };
}
```

This cycle is **not** unique to testing. It's universal.

## The Psychology of TDD: Why It Actually Works

TDD works because it hacks your brain in specific, powerful ways:

### 1. Externalized Intent

When you write a test first, you're forced to articulate **what success looks like** before you start coding.

Without TDD:

```text

"I need to build login... let me start coding...
oh wait, what about password validation...
and session management...
and error cases..."
```

With TDD:

```text

"First test: user can log in with valid credentials.
That's it. That's the only thing I'm building right now."
```

**One clear goal. One feedback mechanism.**

### 2. Objective Judge

Tests remove ambiguity. You're not asking yourself "does this work?" You're asking **the test** whether it works.

Your brain doesn't have to:

- Remember the requirements
- Manually verify edge cases
- Wonder if you broke something
- Question whether you're done

The test tells you.

### 3. Immediate Feedback

The TDD cycle should be **fast**:

- Write test: 30 seconds
- Watch it fail: 2 seconds
- Make it pass: 2 minutes
- Run tests: 5 seconds
- **Total loop time: \~3 minutes**

Compare this to:

- Write code: 30 minutes
- Deploy to staging: 10 minutes
- Manually test: 15 minutes
- Find bug: 5 minutes
- Debug: 20 minutes
- Repeat...
- **Total loop time: 80+ minutes**

The faster your feedback, the faster you learn, the faster you ship.

### 4. Reduced Cognitive Load

You're not holding the entire system in your head. You're focused on:

- **This test**
- **This implementation**
- **This outcome**

Everything else is verified by the **existing** tests.

This is how you build complex systems without going insane.

## The Red-Green-Refactor Cycle: Deeper Than You Think

"Red → Green → Refactor" is taught as a mantra, but let's examine what's really happening:

### Red: Create a Falsifiable Expectation

```javascript
test('calculateTotal handles empty cart', () => {
  expect(calculateTotal([])).toBe(0);
});
```

You're creating a **falsifiable** expectation. This is scientific method applied to code:

- Hypothesis: "An empty cart should total zero"
- Experiment: Run the test
- Result: Pass or fail

**Red** proves your test can fail. This is critical—a test that never fails is worthless.

### Green: Make It Work (Not Perfect)

```javascript
function calculateTotal(items) {
  if (items.length === 0) return 0;
  // That's it. Just make it pass.
}
```

Resist the urge to over-engineer. **Just. Make. It. Pass.**

Why? Because you're building **incrementally**. Each test adds one more constraint. The full solution emerges through accumulation, not up-front design.

### Refactor: Make It Right

```javascript
function calculateTotal(items) {
  // Now we have multiple tests passing
  // We can safely refactor
  return items.reduce((sum, item) => {
    const price = item.price || 0;
    const quantity = item.quantity || 1;
    return sum + (price * quantity);
  }, 0);
}
```

**Refactoring is only safe because you have tests.** Without tests, refactoring is just "changing stuff and hoping."

## TDD Scales to Any Complexity

I hear "TDD doesn't work for complex systems" all the time. This is backwards.

**TDD is MORE valuable for complex systems, not less.**

### Example: Complex State Machine

Imagine building a state machine for order processing:

- New → Pending → Processing → Shipped → Delivered
- With edge cases: cancellations, refunds, partial shipments

Without TDD, you write a massive state machine and manually test every path. **Good luck.**

With TDD, you build it incrementally:

```javascript
// Test 1: Basic transition
test('order transitions from new to pending', () => {
  const order = new Order();
  order.submit();
  expect(order.state).toBe('pending');
});

// Test 2: Can't skip states
test('order cannot go directly to shipped', () => {
  const order = new Order();
  expect(() => order.ship()).toThrow('Invalid state transition');
});

// Test 3: Cancellation from pending
test('order can be cancelled when pending', () => {
  const order = new Order();
  order.submit();
  order.cancel();
  expect(order.state).toBe('cancelled');
});

// Test 4: Cannot cancel after shipped
test('order cannot be cancelled after shipping', () => {
  const order = new Order();
  order.submit();
  order.process();
  order.ship();
  expect(() => order.cancel()).toThrow('Cannot cancel shipped order');
});
```

Each test adds **one rule**. The complexity builds up gradually, and **every rule is verified**.

## TDD and the Dunning-Kruger Effect

There's a fascinating relationship between TDD adoption and the Dunning-Kruger effect:

**Beginners**: "TDD is amazing! I'm writing tests for everything!"

**Intermediate**: "TDD is slowing me down. I know what I'm doing, I don't need tests."

**Advanced**: "TDD is essential. My code is too complex to trust without tests."

The intermediate stage is where most people get stuck. They've gained enough skill to write code quickly, but not enough experience to have been burned by their own bugs repeatedly.

I've been burned. **Many times.** That's why I use TDD.

## What You Can TDD (Hint: Everything)

A common misconception: "TDD only works for business logic."

False. You can TDD:

### 1. Algorithms and Data Structures

```javascript

test('binary search finds element in sorted array', () => {
  expect(binarySearch([1, 3, 5, 7, 9], 5)).toBe(2);
  expect(binarySearch([1, 3, 5, 7, 9], 10)).toBe(-1);
});
```

### 2. API Integrations

```javascript

test('fetches user profile from API', async () => {
  const mockFetch = jest.fn().mockResolvedValue({
    json: async () => ({ id: 1, name: 'John' })
  });

  const profile = await getUserProfile(1, mockFetch);
  expect(profile.name).toBe('John');
});
```

### 3. Database Queries

```javascript

test('finds active users created in last 30 days', async () => {
  const users = await db.users.findActive({ daysAgo: 30 });
  expect(users.every(u => u.active)).toBe(true);
  expect(users.every(u => isRecent(u.createdAt, 30))).toBe(true);
});
```

### 4. UI Component Logic

```javascript

test('form validates email format', () => {
  const form = new ContactForm();
  form.setEmail('invalid-email');

  expect(form.isValid()).toBe(false);
  expect(form.errors.email).toBe('Invalid email format');
});
```

### 5. System Integration

```javascript

test('order creates payment and updates inventory', async () => {
  const order = await processOrder({ item: 'widget', quantity: 5 });

  expect(order.paymentStatus).toBe('charged');
  expect(await getInventory('widget')).toBe(95); // Started at 100
});
```

**If you can verify it, you can TDD it.**

## The TDD Mindset: Expectations and Observations

Here's the key insight that changed how I think about software:

**Engineering is the discipline of creating expectations and observing whether reality matches.**

This applies everywhere:

- Science: hypothesis → experiment → result
- Manufacturing: specification → production → quality control
- **Software**: test → implementation → verification

TDD is simply **making this process explicit and fast**.

## When NOT to TDD (Yes, Really)

TDD isn't dogma. There are legitimate cases where you skip it:

### 1. Spike/Prototype Code

When you're **exploring** an unfamiliar API or architecture, write throwaway code first. Learn. Then TDD the real implementation.

### 2. Truly Trivial Code

```javascript

// Do you really need a test for this?
function getUserName(user) {
  return user.name;
}
```

Use judgment. But be honest about what's "trivial."

### 3. Visual/Interactive Exploration

When you're tweaking CSS or exploring UI interactions, **visual feedback** is your loop. (We'll cover this in Part 2.)

### 4. When You Have a Better Feedback Loop

If you have a REPL, a live reload system, or another fast feedback mechanism—use it. TDD is about the **loop**, not the specific technique.

## TDD Prepares You for the Future

Here's why understanding TDD matters even more in an AI-first world:

**AI agents need feedback loops too.**

When you ask an agent to generate code, the agent needs to know:

- What is the goal? (The test defines it)
- Did it work? (The test verifies it)
- What broke? (The test shows failures)

Tests are **specifications that agents can verify**.

The engineers who understand TDD will naturally understand how to:

- Create verifiable expectations for agents
- Design objective evaluation criteria
- Build systems that agents can iterate on safely

## Practical TDD: Starting Tomorrow

If you want to adopt TDD, start small:

### Week 1: Write Tests for Bugs

When you find a bug:

1. Write a failing test that reproduces it
2. Fix the bug
3. Confirm the test passes

You're building a regression suite **and** practicing the TDD cycle.

### Week 2: TDD One Function Per Day

Pick one new function per day. TDD it. Just one.

### Week 3: TDD One Feature

Pick a small feature. Commit to TDD-ing the whole thing.

### Week 4: Reflect

Did TDD help? Where did it slow you down? Where did it catch bugs?

**Adjust your approach based on feedback.** (See what I did there?)

## The Foundation for Everything Else

TDD teaches you the **fundamental pattern** of software engineering:

1. Create an expectation
2. Observe the outcome
3. Iterate

This pattern applies to:

- Frontend development (Part 2)
- UI component libraries
- API design
- System architecture
- **AI agent orchestration** (Part 3)

Master TDD, and you're mastering the **universal pattern of feedback-driven development**.

---

**Next**: [Part 2 - UI Feedback Patterns: Hot Reload, Storybook, and Visual Development](https://kobi.kadosh.me/blog/feedback-loops-part-2-ui)

**Read the full overview**: [The Software Feedback Loop: From TDD to Agentic AI Development](https://kobi.kadosh.me/blog/feedback-loops-software-engineering)

---

_This post was co-created with Claude Code, using the feedback-driven approach it describes._
