Katas for Test-Driven Development (TDD)
The content here is under the Attribution 4.0 International (CC BY 4.0) license
Join Our Community
Connect with developers, architects, and tech leads who share your passion for quality software development. Discuss TDD, architecture, software engineering, and more.
→ Join SlackCode katas are focused programming exercises designed to help you practice fundamental skills through deliberate practice. Rather than learning TDD only through production code under deadline pressure, katas provide a low-stakes environment where you can develop fluency with testing practices, refactoring techniques, and design patterns.
The term “kata” comes from martial arts—choreographed patterns practiced repeatedly to develop technique. In programming, katas serve the same purpose: building muscle memory and developing instinctive problem-solving approaches through focused repetition.
Why practice katas?
Before diving into specific katas, understand the value of structured practice:
Deliberate practice builds expertise: Research on skill acquisition demonstrates that expertise develops not from accumulated experience, but from structured practice that pushes you beyond your comfort zone while providing immediate feedback. Production code rarely offers this—deadlines and business pressure limit the time for refinement.
Katas isolate specific techniques: Production work requires juggling multiple concerns: business logic, database interaction, API communication, error handling, logging. Katas isolate one or two techniques, allowing deep focus. String Calculator teaches TDD rhythm without complexity. Bank Kata teaches mocking. Gilded Rose teaches refactoring.
Safe environment for experimentation: In production code, mistakes have consequences. In katas, mistakes are free. This psychological safety enables trying multiple approaches, understanding trade-offs, and building confidence.
Transfer learning to production: Skills practiced in katas transfer to production code when the kata mirrors production challenges. A kata practicing async/await patterns directly applies when handling API calls in production. A kata on state management patterns applies when building Redux reducers.
Measuring growth: Katas are repeatable. Practicing FizzBuzz monthly lets you observe improvements: write it faster, think about refactoring earlier, consider more test cases. This visible progress is motivating.
The TDD kata cycle
Each kata follows the red-green-refactor cycle:
1. Red: Write a failing test that defines the next small behavior
- Test names complete the phrase “should…”
- Start with simplest failing case
- Avoid solving too much at once
2. Green: Write minimal code to make the test pass
- Hardcoding answers is acceptable initially
- Focus on making tests pass, not on clean code
- Duplication is okay at this stage
3. Refactor: Improve code quality while keeping tests passing
- Remove duplication
- Extract helper functions
- Improve naming
- Never add new functionality during refactoring
4. Repeat: Write the next failing test
This rhythm builds instinctive TDD thinking that transfers to production code.
Kata catalog by difficulty
Beginner katas
These katas teach TDD fundamentals with simple problem domains.
FizzBuzz
Purpose: Master the red-green-refactor cycle and learn when/how to refactor
Problem: Generate numbers 1-100, but:
- For multiples of 3, print “Fizz”
- For multiples of 5, print “Buzz”
- For multiples of both, print “FizzBuzz”
Why it’s valuable: FizzBuzz appears deceptively trivial, but practicing it repeatedly reveals the discipline of TDD. The value isn’t in solving it (you can solve it in minutes), but in solving it test-first with multiple refactoring approaches:
- Approach 1 - Procedural: Use nested if statements
- Approach 2 - Rules engine: Store rules in data structure
- Approach 3 - Functional: Use composition of filter functions
- Approach 4 - Object-oriented: Implement rule objects with polymorphism
Repeating FizzBuzz with different approaches builds refactoring intuition.
Suggested test cases:
test('should return 1 for number 1', () => {
expect(fizzBuzz(1)).toBe('1');
});
test('should return Fizz for multiples of 3', () => {
expect(fizzBuzz(3)).toBe('Fizz');
});
test('should return Buzz for multiples of 5', () => {
expect(fizzBuzz(5)).toBe('Buzz');
});
test('should return FizzBuzz for multiples of 15', () => {
expect(fizzBuzz(15)).toBe('FizzBuzz');
});
Duration: 30-45 minutes
Related concepts: Control flow, conditionals, basic TDD cycle
String Calculator
Purpose: Practice incremental test-driven development with incrementally complex requirements
Problem: Create a function that:
- Takes a string of numbers: “1” or “1,2” or “1,2,3”
- Returns the sum
- Handles new line delimiters: “1\n2”
- Supports custom delimiters: “//;\n1;2”
- Ignores numbers greater than 1000
- Throws exception on negative numbers
Why it’s valuable: This kata teaches incremental development—starting simple and growing complexity step-by-step. Each requirement is self-contained, teaching how to refactor as complexity arrives.
Test progression (write tests in this order):
// Stage 1: Basic sum
test('empty string returns 0', () => {
expect(add('')).toBe(0);
});
test('single number returns itself', () => {
expect(add('1')).toBe(1);
});
// Stage 2: Comma delimiter
test('comma-delimited numbers are summed', () => {
expect(add('1,2,3')).toBe(6);
});
// Stage 3: Newline delimiter
test('handles newline delimiters', () => {
expect(add('1\n2\n3')).toBe(6);
});
// Stage 4: Custom delimiter
test('custom delimiter specified in prefix', () => {
expect(add('//;\n1;2;3')).toBe(6);
});
// Stage 5: Numbers > 1000 ignored
test('ignores numbers greater than 1000', () => {
expect(add('2,1001,3')).toBe(5);
});
// Stage 6: Negative number exception
test('throws exception on negative numbers', () => {
expect(() => add('1,-2,3')).toThrow();
});
Duration: 60-90 minutes
Related concepts: Parsing, error handling, incremental refactoring, test-driven design
Roman Numerals
Purpose: Practice decomposition and test-driven design
Problem: Convert integers to Roman numeral representation
- 1 = I, 5 = V, 10 = X, 50 = L, 100 = C, 500 = D, 1000 = M
- 4 = IV, 9 = IX, 40 = XL, 90 = XC, 400 = CD, 900 = CM
Why it’s valuable: Roman numerals have a clear mapping structure, teaching how to identify and extract patterns. It practices thinking about the problem domain before coding.
Test cases:
test('1 converts to I', () => {
expect(toRoman(1)).toBe('I');
});
test('4 converts to IV (subtractive notation)', () => {
expect(toRoman(4)).toBe('IV');
});
test('58 converts to LVIII', () => {
expect(toRoman(58)).toBe('LVIII');
});
test('1994 converts to MCMXCIV', () => {
expect(toRoman(1994)).toBe('MCMXCIV');
});
Duration: 45-60 minutes
Related concepts: Mapping, lookup tables, subtractive principles
Intermediate katas
These katas introduce state management, mocking, and more complex design decisions.
Bowling Game
Purpose: Manage complex state and practice outside-in TDD
Problem: Calculate bowling score rules:
- Single frame = pins knocked down (0-10)
- 10 frames total
- Spare (all 10 pins in two rolls): score = 10 + next roll
- Strike (all 10 pins in one roll): score = 10 + next two rolls
- 10th frame: bonus rolls if strike/spare
- Total score = sum of all frame scores
Why it’s valuable: Bowling game introduces state that must be tracked across calls. This requires thinking about the object interface before implementation—classic outside-in TDD. Score calculation has intricate dependencies between frames, teaching how to manage coupling.
Test progression:
test('gutter game (all zeros) = 0', () => {
const game = new BowlingGame();
// 20 rolls of 0 pins
for (let i = 0; i < 20; i++) {
game.roll(0);
}
expect(game.score()).toBe(0);
});
test('all ones = 20', () => {
const game = new BowlingGame();
for (let i = 0; i < 20; i++) {
game.roll(1);
}
expect(game.score()).toBe(20);
});
test('one spare = 10 + next roll + remaining rolls', () => {
const game = new BowlingGame();
game.roll(5);
game.roll(5); // spare
game.roll(3);
// ... remaining rolls
expect(game.score()).toBe(16 + minimumRemaining);
});
test('one strike = 10 + next two rolls + remaining rolls', () => {
const game = new BowlingGame();
game.roll(10); // strike
game.roll(3);
game.roll(4);
// ... remaining rolls
expect(game.score()).toBe(17 + minimumRemaining);
});
test('perfect game (all strikes) = 300', () => {
const game = new BowlingGame();
for (let i = 0; i < 12; i++) {
game.roll(10);
}
expect(game.score()).toBe(300);
});
Duration: 90-120 minutes
Related concepts: Complex state, boundary conditions, object design
Mars Rover
Purpose: Practice state management with command patterns
Problem: Simulate a rover on Mars:
- Rover receives commands as string: “L” (turn left), “R” (turn right), “M” (move forward)
- Rover has position (x,y) and direction (N, S, E, W)
- Execute command sequence and return final position/direction
Why it’s valuable: Mars Rover is a classic kata for learning state pattern. Early implementations use nested if statements; refactoring discovers the state pattern naturally. This teaches how design patterns emerge from requirements rather than being imposed.
Example:
// Input: position (1,2) facing North, commands "LMLMLMLMM"
// Expected: position (1,3) facing North
test('single step forward', () => {
const rover = new Rover(0, 0, 'N');
rover.execute('M');
expect(rover.position()).toBe({ x: 0, y: 1, direction: 'N' });
});
test('turn left', () => {
const rover = new Rover(0, 0, 'N');
rover.execute('L');
expect(rover.position()).toBe({ x: 0, y: 0, direction: 'W' });
});
test('complex command sequence', () => {
const rover = new Rover(1, 2, 'N');
rover.execute('LMLMLMLMM');
expect(rover.position()).toBe({ x: 1, y: 3, direction: 'N' });
});
Duration: 60-90 minutes
Related concepts: State pattern, command pattern, immutable design
Bank Kata
Purpose: Practice mocking and outside-in TDD
Problem: Implement a bank account with:
- Deposit money
- Withdraw money
- Print statement with transaction history
- Each transaction: date, amount, balance
Why it’s valuable: Bank Kata teaches mocking (how to test that the “print” method was called with correct format without actually printing). It practices thinking about the dependency graph—the account depends on a printer abstraction, which can be mocked in tests.
Test approach (using mocks):
test('deposit increases balance', () => {
const account = new Account();
account.deposit(100);
expect(account.balance()).toBe(100);
});
test('withdraw decreases balance', () => {
const account = new Account();
account.deposit(100);
account.withdraw(30);
expect(account.balance()).toBe(70);
});
test('statement prints all transactions', () => {
const mockPrinter = {
print: jest.fn()
};
const account = new Account(mockPrinter);
account.deposit(100);
account.withdraw(30);
account.printStatement();
expect(mockPrinter.print).toHaveBeenCalledWith(
expect.stringContaining('100')
);
expect(mockPrinter.print).toHaveBeenCalledWith(
expect.stringContaining('70')
);
});
Duration: 75-105 minutes
Related concepts: Mocking, dependency injection, interface design
Advanced katas
These katas teach refactoring and complex business logic in existing code.
Gilded Rose
Purpose: Master refactoring, characterization tests, and approval testing
Problem: Refactor existing code (intentionally poorly written) that manages an inventory system:
- Normal items: quality degrades by 1 each day, value degrades by 1 each day
- Aged brie: quality improves by 1 each day
- Sulfuras: never changes
- Concert tickets: quality increases as event approaches, drops to 0 after
- Conjured items: quality degrades twice as fast
Why it’s valuable: Gilded Rose is uniquely valuable because you start with broken, hard-to-understand code. This mirrors real development where you inherit legacy code. The kata teaches:
- Characterization tests: Write tests documenting actual behavior before refactoring
- Safe refactoring: With comprehensive tests, refactor with confidence
- Incremental improvement: Improve piece by piece, keeping tests passing
Approach:
// Stage 1: Write characterization tests (describe observed behavior)
test('normal item degrades by 1 each day', () => {
const items = [new Item('normal', 10, 10)];
const shop = new Shop(items);
shop.updateQuality();
expect(items[0].quality).toBe(9);
expect(items[0].sellIn).toBe(9);
});
test('aged brie improves by 1 each day', () => {
const items = [new Item('Aged Brie', 10, 10)];
const shop = new Shop(items);
shop.updateQuality();
expect(items[0].quality).toBe(11);
});
// Stage 2: Extract methods and simplify
// Stage 3: Use state pattern or strategy pattern
// Stage 4: Make code expressive and maintainable
Duration: 120-180 minutes
Related concepts: Characterization tests, approval testing, incremental refactoring, design patterns
Racing Car Kata
Purpose: Practice SOLID principles (especially Single Responsibility)
Problem: Refactor a complex “racer” class that:
- Manages car movement (position, velocity)
- Handles input (accelerate, brake)
- Renders display output
- Plays sound effects
- Logs telemetry
- All in one massive class
Why it’s valuable: Racing Car teaches how to recognize Single Responsibility violations and extract classes. The goal is reducing the original ~300 line class to cohesive, independently testable pieces. This teaches:
- Recognizing when a class has too many reasons to change
- Extracting responsibilities into separate classes
- Designing interfaces between components
- Testing components in isolation
Refactoring steps:
- Extract output rendering → separate Renderer class
- Extract sound management → separate SoundPool class
- Extract movement logic → separate Physics class
- Extract logging → separate Telemetry class
Result: Small, focused classes each testable independently.
Duration: 120-150 minutes
Related concepts: SOLID principles, Single Responsibility, dependency injection, composition
Getting started with katas
Choose your kata progression
Option 1 - Beginner path (6-8 weeks, 1-2 hours/week):
- FizzBuzz (repeat 2-3 times with different approaches)
- String Calculator
- Roman Numerals
- FizzBuzz again (compare to first attempt)
Option 2 - Intermediate path (8-12 weeks, 2-3 hours/week):
- Complete beginner path
- Bowling Game
- Mars Rover
- Bank Kata
Option 3 - Advanced path (12+ weeks, 3-5 hours/week):
- Complete intermediate path
- Gilded Rose (start here if you have refactoring experience)
- Racing Car Kata
- Repeat previous katas at faster pace with new approaches
Practice guidelines
Frequency: Practice 2-3 times per week for consistent improvement. Daily practice accelerates learning but risks burnout.
Duration: Start with 30-45 minutes per session. Build to 90-120 minute sessions for complex katas.
Multiple approaches: After completing a kata successfully, restart immediately and implement it differently. If you used procedural if/then logic, try object-oriented patterns. If you used loops, try functional approaches.
Deliberate reflection: After each session, reflect on:
- Did you write tests first or after implementation?
- How many times did you refactor?
- What surprised you?
- What was hardest?
- How would you approach it differently next time?
Pair programming: Practice katas with a more experienced developer. Pair programming accelerates learning significantly.
Related subjects
- Katas: Why, when, and how - Deep dive into kata philosophy and learning
- A gentle introduction to TDD - TDD fundamentals
- TDD anti-patterns - Common mistakes to avoid
Resources
- Emily Bache’s GitHub - Kata repositories - High-quality kata implementations
- Code Wars - Gamified kata practice platform
- CodingGame - Kata practice with immediate feedback
- My katas repository - Personal kata implementations and solutions
References
- Kent Beck, Test Driven Development: By Example
- Martin Fowler, Refactoring: Improving the Design of Existing Code
- Robert C. Martin, Clean Architecture: A Craftsman’s Guide to Software Structure and Design
Changelog
- Feb 15, 2026 - Initial comprehensive kata guide with difficulty levels, specific examples, and progression paths
About this post
This post content s was assisted by AI, which helped with research, curate content and code suggestions.