DEV Community

Chandana prabhakar
Chandana prabhakar

Posted on

From Red to Green: What I Learned Diving into Test-Driven Development (TDD)

I’ve been hands-on with Test-Driven Development (TDD)—a practice where you write tests before you write production code. What initially seemed backwards, ended up completely transforming how I think about building reliable software.

I used to write code like this:

  1. Hack together a feature
  2. Manually test it in the browser/Postman
  3. Fix bugs
  4. Repeat until it mostly works

Then I discovered Test-Driven Development (TDD), and everything changed. Now, I write code like this:

  1. Write a failing test (Red)
  2. Make it pass with minimal code (Green)
  3. Clean up without fear (Refactor)

And guess what? I ship fewer bugs, refactor with confidence, and actually enjoy coding more. If that sounds like magic, let me break it down.

Why Write Tests First?

  • Clarify expected behavior before diving into implementation.
  • Avoid untested code, which reduces hidden bugs.
  • Maintain cleaner code by validating it continuously.

What Is TDD?

Test-driven development (TDD) involves writing tests for your production code before writing the actual code.

The RedGreenRefactor cycle isn't just workflow; it's a cognitive framework that:

  1. Red Phase: Forces explicit requirement articulation through failing assertions
  2. Green Phase: Drives minimal viable implementation
  3. Refactor Phase: Enables fearless architectural evolution with regression safety nets

TDD Workflow

  1. Red Phase (Fail First)
    Before implementing the ShoppingCart class, write a test that describes the expected behavior: "After adding a $10 item, the cart total should be $10."

    Watch it fail (red test) - because the ShoppingCart class has not been implemented yet.

describe("ShoppingCart", () => {
  it("should have a total of $10 after adding a $10 item", () => {
    const cart = new ShoppingCart();
    cart.addItem({ price: 10 });
    expect(cart.total).to.equal(10); //This will fail (RED) if ShoppingCart isn't implemented
  });
});
Enter fullscreen mode Exit fullscreen mode

Why? Because ShoppingCart doesn’t exist yet!

  1. Green Phase (Make It Work) Write just enough code to pass this one test Not perfect code - just functional code
class ShoppingCart {
  constructor() {
    this.total = 0;
  }

  addItem(item) {
    this.total += item.price; // Now passes (GREEN)
  }
}
Enter fullscreen mode Exit fullscreen mode

No over-engineering. Just make the test pass.

  1. Refactor Phase (Make It Nice) Now improve the code with confidence Your test tells you if you break anything
class ShoppingCart {
  constructor() {
    this.items = []; // Better structure!
    this.total = 0;
  }

  addItem(item) {
    this.items.push(item);
    this.total = this.items.reduce((sum, item) => sum + item.price, 0);
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the test again. Still passes? Great. Broke something? Fix it now, not in production.

The Testing Pyramid

🔺 E2E Tests (5-10%)
├─ Contract Tests
├─ API Integration Tests  
🔺 Integration Tests (20-30%)
├─ Service Layer Tests
├─ Database Integration Tests
🔺 Unit Tests (60-70%)
├─ Pure Function Tests
├─ Class Behavior Tests
└─ Mock-based Isolation Tests
Enter fullscreen mode Exit fullscreen mode
  • Unit Tests: Focus on small, isolated pieces like functions.
  • Integration Tests: Ensure different modules work together (e.g., DB + API), Third-party service integration points
  • End-to-End Tests: Simulate real user flows in the app, Critical business process flows, Cross-service communication paths, UI/UX interaction patterns.

The RITE Framework

  • Readable : Understandable by anyone on the team.
describe('Invoice Generation', () => {
  it('should include 10% early payment discount for payments within 15 days', () => {
    // Arrange: Set up the scenario
    const invoice = createInvoice({ amount: 1000, dueDate: '2024-01-15' });
    const paymentDate = '2024-01-10'; // 5 days early

    // Act: Execute the behavior
    const finalAmount = calculatePaymentAmount(invoice, paymentDate);

    // Assert: Verify the outcome
    expect(finalAmount).to.equal(900); // 10% discount applied
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Isolated : Doesn’t rely on other tests.
// Tests that depend on each other
let userId;
it('should create user', () => {
  userId = createUser({ email: 'test@example.com' });
});
it('should update user email', () => {
  updateUser(userId, { email: 'new@example.com' });
});

// Self-contained tests
it('should update user email', () => {
  const user = createTestUser({ email: 'test@example.com' });
  updateUser(user.id, { email: 'new@example.com' });
  expect(getUserEmail(user.id)).to.equal('new@example.com');
});
Enter fullscreen mode Exit fullscreen mode
  • Thorough : Covers edge cases, not just the happy path.
describe('Password Validation', () => {
  // Happy path
  it('should accept valid strong passwords', () => {
    expect(validatePassword('SecurePass123!')).to.be.true;
  });

  // Edge cases that matter
  it('should reject passwords shorter than 8 characters', () => {
    expect(validatePassword('Pass1!')).to.be.false;
  });

  it('should reject passwords without special characters', () => {
    expect(validatePassword('Password123')).to.be.false;
  });

  it('should handle empty and null inputs gracefully', () => {
    expect(validatePassword('')).to.be.false;
    expect(validatePassword(null)).to.be.false;
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Explicit : All requirements are visible, no hidden setup.
// Hidden test setup
beforeEach(() => {
  setupDatabase();
  createAdminUser();
  seedTestData();
});

// Explicit test context
it('should allow admin users to delete orders', () => {
  const adminUser = createUser({ role: 'admin' });
  const order = createOrder({ status: 'pending', userId: 123 });

  const result = deleteOrder(order.id, adminUser);

  expect(result.success).to.be.true;
  expect(getOrder(order.id)).to.be.null;
});
Enter fullscreen mode Exit fullscreen mode

Tools I Used

For testing in JavaScript, stack included:

Mocha: Test runner
Chai: Assertion library
Sinon: For mocks/stubs
Supertest: For HTTP testing
NYC: For code coverage

Final Thoughts

TDD isn't just about preventing bugs—it's about elevating the entire discipline of software engineering. It's about building systems that are not just functional, but predictable, maintainable, and evolutionarily robust.

The question isn't whether you can afford to adopt TDD—it's whether you can afford not to in an industry where software complexity grows exponentially.

Start your TDD journey today: start small, write clearly, and trust the process. Pick one critical module, write that first failing test, and experience the paradigm shift firsthand.

Give it a shot!

Want to See TDD in Action? Check My Code!
I’ve published a practical TDD example on GitHub:

https://github.com/ChandanaPrabhakar/TDD-Workspace.git
Clone it, run the tests, and hack around!

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.