Skip to content

TDD Exercise: Fix the Counter Component

Scenario Description

You've inherited a counter Svelte component. The tests are written but the implementation has bugs. You need to fix these issues using TDD methodology.

Requirements Specification

  1. Counter starts at 0
  2. Clicking "+" button increments count by 1
  3. Clicking "-" button decrements count by 1
  4. Clicking "Reset" button sets count to 0
  5. Counter has boundaries: minimum -10, maximum 10

Initial Code

Counter Component

svelte
<!-- Counter.svelte -->
<script>
  let count = 0;
  
  function increment() {
    count++;
  }
  
  function decrement() {
    count--;
  }
  
  function reset() {
    count = 0;
  }
</script>

<div class="counter">
  <span class="count">{count}</span>
  <button on:click={decrement}>-</button>
  <button on:click={increment}>+</button>
  <button on:click={reset}>Reset</button>
</div>

Test File

javascript
// Counter.test.js
import { render, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';

describe('Counter', () => {
  test('initial value should be 0', () => {
    const { getByText } = render(Counter);
    expect(getByText('0')).toBeInTheDocument();
  });

  test('clicking + button should increment count', async () => {
    const { getByText } = render(Counter);
    await fireEvent.click(getByText('+'));
    expect(getByText('1')).toBeInTheDocument();
  });

  test('clicking - button should decrement count', async () => {
    const { getByText } = render(Counter);
    await fireEvent.click(getByText('-'));
    expect(getByText('-1')).toBeInTheDocument();
  });

  test('count should not go below -10', async () => {
    const { getByText } = render(Counter);
    
    // Click 12 times
    for (let i = 0; i < 12; i++) {
      await fireEvent.click(getByText('-'));
    }
    
    expect(getByText('-10')).toBeInTheDocument();
  });

  test('count should not exceed 10', async () => {
    const { getByText } = render(Counter);
    
    for (let i = 0; i < 12; i++) {
      await fireEvent.click(getByText('+'));
    }
    
    expect(getByText('10')).toBeInTheDocument();
  });

  test('reset button should set count to zero', async () => {
    const { getByText } = render(Counter);
    
    await fireEvent.click(getByText('+'));
    await fireEvent.click(getByText('+'));
    await fireEvent.click(getByText('Reset'));
    
    expect(getByText('0')).toBeInTheDocument();
  });
});

Your Task

  1. Run tests, observe which tests fail
  2. Analyze the problem, understand why they fail
  3. Fix using TDD:
    • Confirm failing tests (RED)
    • Write minimal code to pass (GREEN)
    • Refactor code (REFACTOR)
  4. Ensure all tests pass

TDD Process Guide

Step 1: RED (Observe Failure)

bash
npm test

Observe output:

  • Which tests fail?
  • What are the failure reasons?

Expected result: Boundary tests fail (count exceeds -10 and 10 limits)

Step 2: GREEN (Make It Pass)

Follow TDD principles:

  • Only modify the minimal code needed to pass tests
  • Don't over-engineer
  • Don't add features not required by tests

Step 3: REFACTOR (Clean Up)

After tests pass:

  • Is the code clear?
  • Is there duplication?
  • Does it follow project conventions?

Hints

Click to expand hints

Hint 1: Boundary Checking

Tests require upper and lower limits:

  • Minimum: -10
  • Maximum: 10

Current code has no boundary checks.

Hint 2: increment Function

javascript
function increment() {
  if (count < 10) {  // Add upper limit check
    count++;
  }
}

Hint 3: decrement Function

javascript
function decrement() {
  if (count > -10) {  // Add lower limit check
    count--;
  }
}

Hint 4: Don't Modify Tests

Core TDD principle: Tests are specifications. If tests are correct, fix the code not the tests.

Reference Solution

Click to expand reference solution
svelte
<!-- Counter.svelte -->
<script>
  let count = 0;
  const MIN = -10;
  const MAX = 10;
  
  function increment() {
    if (count < MAX) {
      count++;
    }
  }
  
  function decrement() {
    if (count > MIN) {
      count--;
    }
  }
  
  function reset() {
    count = 0;
  }
</script>

<div class="counter">
  <span class="count">{count}</span>
  <button on:click={decrement}>-</button>
  <button on:click={increment}>+</button>
  <button on:click={reset}>Reset</button>
</div>

<style>
  .counter {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }
  .count {
    font-size: 1.5rem;
    min-width: 2rem;
    text-align: center;
  }
  button {
    padding: 0.25rem 0.5rem;
  }
</style>

Key Changes

  1. Add constants: MIN and MAX define boundaries
  2. Boundary checks: increment and decrement check boundaries
  3. Keep it simple: Only add features required by tests, no over-engineering

Key Learning Points

1. RED-GREEN-REFACTOR Cycle

   ┌───────┐     ┌───────┐
   │ RED   │────►│ GREEN │
   │(fail) │     │(pass) │
   └───────┘     └───┬───┘
       ▲             │
       │        ┌────▼────┐
       │        │REFACTOR │
       └────────│         │
                └─────────┘

2. Tests as Specifications

Tests aren't just verification tools—they're behavior specifications. By reading tests, you should understand complete component behavior.

3. Minimal Change Principle

TDD emphasizes writing only the minimum code to pass tests. This avoids over-engineering and unnecessary complexity.

4. Don't Modify Passing Tests

If tests are reasonable but code doesn't pass, fix the code not the tests.

Common Mistakes

MistakeCorrect Approach
Write code before testsWrite tests first, observe failure, then write code
Modify tests to make them passModify code to satisfy tests
Add features not required by testsOnly implement behavior required by tests
Skip RED phaseMust see tests fail before writing code

Advanced Exercises

After completing the basic exercise, try:

  1. Add step size: Allow users to set increment/decrement step value
  2. Display status: Show warning when boundary is reached
  3. Keyboard support: Support arrow key controls