Practicing TDD with Katas: String Calculator, Bowling Game and Deliberate Repetition
A kata is practicing the TDD rhythm through deliberate repetition, solving a small problem with a known answer many times; success means step quality, not finishing. We cover coding dojo, randori and ping-pong formats, and solve String Calculator with C# and xUnit v3 Theory and Bowling Game with TypeScript and Vitest, following the test list, the critical decision points and variations that make a kata harder, with real output.
Version note: The code in this post was built and run with .NET 10 and xUnit.net v3 4.0.3, and the TypeScript examples with Node.js 24 and Vitest 5.1.0.
Predict First
In the first step of the String Calculator kata you write an Add(string) method: an empty string returns 0, "1" returns 1, "1,2" returns 3. To make the third test pass you wrote this line:
public static int Add(string numbers) =>
numbers.Length == 0 ? 0 : numbers.Split(',').Sum(int.Parse);
The second step of the kata says: the method should handle an unknown number of numbers. You add [InlineData("1,2,3,4", 10)] for that. Is the test red or green? If it's green, is that good news or bad news? Pick an answer; we come back to this question several times during the kata.
What Is a Kata and Why Solve the Same Problem Again?
The term "code kata" was popularized in the early 2000s by Dave Thomas, one of the authors of The Pragmatic Programmer. The word comes from martial arts: a kata is a fixed sequence of moves a master has students repeat over and over. The goal is not to win a fight but to repeat the move so many times that when the real moment comes you do it right without thinking.
The software equivalent: you pick a small, well-bounded problem whose answer is already known and solve it many times. Knowing the answer is not a problem here; on the contrary, it is a requirement. When the problem doesn't tire you, you can pay attention to the process: how you built the test list, which test you picked first, how big your steps were, where you refactored and where you skipped it. TDD is a skill, and skills grow through deliberate repetition in a safe setting, not under the pressure of production code.
That's why the success criterion of a kata session is not finishing the kata. In a thirty-minute session, finishing four of String Calculator's nine steps while seeing red for the right reason every time and cleaning tests and code on every green is far more valuable than finishing all nine in an hour with a single giant commit. Roy Osherove's String Calculator page points the same way: do the kata for a short time every day, don't read ahead, take the steps one at a time.
The time box is the tool for this. In formats such as code retreat, the code is deleted at the end of every session: the goal is not to accumulate code but to walk the same path better next time. If you hesitate to delete it, that's a sign you have started to see the kata as production code.
Coding Dojo, Randori and Pairing Formats
A kata can be done alone, but the real learning comes from watching other people's steps. A coding dojo is the name of a group that gets together for this. The history on codingdojo.org traces the idea from Dave Thomas's katas to a call for a dojo by Laurent Bossavit and the Paris dojo that started with Emmanuel Gaillot. The sessions in Paris lasted an hour to an hour and a half and usually started from an empty file.
There are two common formats. In a prepared kata one person prepares the solution in advance and writes it from scratch in front of the group, with TDD, in small steps, explaining each one. The audience interrupts only when they don't understand something. It is good for showing a new technique or a language. In a randori there is no preparation: the group picks a kata, two people take the keyboard and write while talking through their thinking, and the audience makes suggestions, mostly while the tests are green. The driver changes according to a rule.
Two of the most useful switching rules are these. Time box: each pair stays at the keyboard for 5–7 minutes; when time is up the driver goes back to the audience, the copilot becomes the driver and someone from the audience becomes the new copilot. Ping-pong: one writes a red test, the other makes it pass, refactors, writes the next red test and hands the keyboard back. Because ping-pong separates the person who writes the test from the person who writes the code, it teaches "the simplest thing that makes the test pass" very well: passing your partner's test with a fake forces them to write a better test.
String Calculator: The Test List and the First Steps
String Calculator is the first of Roy Osherove's TDD katas. The kata grows a method that sums the numbers in a string step by step: first a few comma-separated numbers, then a newline separator, then a custom separator declared on the first line, then an exception for negative numbers, and further steps beyond. With our own take, in C# and xUnit v3, we solve the first six steps. Our test list:
// Test list: StringCalculator.Add
// [ ] Empty string: 0
// [ ] One number: itself
// [ ] Two numbers, comma separated: their sum
// [ ] An unknown amount of numbers
// [ ] Newline is a separator too: "1\n2,3" -> 6
// [ ] Custom separator on the first line: "//;\n1;2" -> 3
// [ ] Negative numbers throw, and the message lists all of them
The first two items are very small steps. For the empty string we write return 0; and go green (fake it). For a single number we add int.Parse. Since these are different examples of the same behavior, we gather them in a [Theory]; the details of data-driven tests are in the Theory post of the Unit Test series. When we add the third data row, the red comes not from an assertion but from an exception:
failed Kata.Tests.StringCalculatorTests.Add_returns_the_sum_of_the_numbers(numbers: "1,2", expected: 3) (5ms)
Xunit.MicrosoftTestingPlatform.XunitException: System.FormatException : The input string '1,2' was not in a correct format.
at Kata.StringCalculator.Add(String numbers) in src/Kata/StringCalculator.cs:5
…
total: 3
failed: 1
succeeded: 2
Each data row of a Theory is reported as a separate test, and you see the parameters in the test name; you don't have to search for the example that broke. With the Split line from "Predict First", three tests are green.
String Calculator: Critical Decision Points
Now the answer to the prediction. After adding the "1,2,3,4" row, the test is green on the first run. The kata's second step was solved inside the first one without us noticing: Split with Sum doesn't care how many numbers there are. Is that good news? Partly. The behavior is right, but from the kata's point of view we skipped a step. Had we written parts[0] + parts[1] with fake it at the third test, the second step would have forced us to the real generalization and we would have experienced triangulation. In a kata this is not a mistake but an observation: next time you can try making the step smaller on purpose. The value of repeating a kata comes exactly from observations like this.
The newline item is red again with a FormatException. The fix is Split([',', '\n']). The custom separator item brings the first real design decision:
failed Kata.Tests.StringCalculatorTests.Add_returns_the_sum_of_the_numbers(numbers: "//;\n1;2", expected: 3) (0ms)
Xunit.MicrosoftTestingPlatform.XunitException: System.FormatException : The input string '//;' was not in a correct format.
The input now has two parts: an optional header line and the numbers. The first green was a few lines that handled the header inside Add. When we moved on to the negative numbers item, Add started doing two jobs: parsing the input and applying a rule. After turning the negatives test green, we moved parsing into its own method:
namespace Kata;
public static class StringCalculator
{
public static int Add(string numbers)
{
if (numbers.Length == 0)
{
return 0;
}
int[] values = Parse(numbers);
int[] negatives = [.. values.Where(v => v < 0)];
if (negatives.Length > 0)
{
throw new ArgumentException($"Negatives not allowed: {string.Join(", ", negatives)}");
}
return values.Sum();
}
private static int[] Parse(string input)
{
char[] delimiters = [',', '\n'];
if (input.StartsWith("//"))
{
delimiters = [.. delimiters, input[2]];
input = input[4..];
}
return [.. input.Split(delimiters).Select(int.Parse)];
}
}
The final tests:
public sealed class StringCalculatorTests
{
[Theory]
[InlineData("", 0)]
[InlineData("1", 1)]
[InlineData("1,2", 3)]
[InlineData("1,2,3,4", 10)]
[InlineData("1\n2,3", 6)]
[InlineData("//;\n1;2", 3)]
public void Add_returns_the_sum_of_the_numbers(string numbers, int expected)
{
int sum = StringCalculator.Add(numbers);
Assert.Equal(expected, sum);
}
[Fact]
public void Add_rejects_negatives_and_lists_them_all()
{
ArgumentException ex = Assert.Throws<ArgumentException>(() => StringCalculator.Add("1,-2,3,-4"));
Assert.Equal("Negatives not allowed: -2, -4", ex.Message);
}
}
Test run summary: Passed!
total: 7
failed: 0
succeeded: 7
skipped: 0
duration: 297ms
The first red of the negatives test is instructive too: Assert.Throws() Failure: No exception was thrown. Since the message must list all the negatives, not just one, the test data has two negatives on purpose; a test written with a single negative would also show green for a half-done implementation that throws at the first negative.
Bowling Game: With TypeScript and Vitest
The second kata is Bowling Game, which Robert C. Martin (Uncle Bob) has been presenting for many years. The problem fits in one sentence: given the rolls of a ten-frame bowling game, calculate the total score. The rule details matter here: knocking down all ten pins with two rolls in a frame is a spare, and the next single roll is added as a bonus; knocking them all down with the first roll is a strike, and the next two rolls are the bonus. In the last frame the bonus rolls are played, and a perfect game is 300.
This time we solve it with TypeScript and Vitest. A kata doesn't need a framework; npm install -D vitest in an empty folder is enough. Vitest transforms and runs the TypeScript file without type-checking it, which keeps the loop fast. Setting up Vitest in Angular projects is a topic for the upcoming Frontend Testing series.
The first test is the gutter game: all twenty rolls are zero, the score is zero. Since there is no module yet, the first red comes from Vitest: Error: Cannot find module './bowling-game'. We go green with a class whose score() returns 0. The second test: all twenty rolls are one, the score is twenty:
AssertionError: expected +0 to be 20 // Object.is equality
Adding the rolls to a total inside roll() is the obvious implementation. Two tests green. In the refactor step we gather the loop repeated in the tests into a rollMany helper.
Bowling Game: Realizing the Design Is Wrong
The kata's most famous moment arrives with the third test. A spare: 5 and 5, then 3, the rest zeros. The score should be 10 + 3 bonus + 3 = 16.
× adds the next roll as a spare bonus 2ms
AssertionError: expected 13 to be 16 // Object.is equality
Tests 1 failed | 2 passed (3)
To turn this red green you could write an if in roll() like "if the previous two rolls added up to ten, count this roll twice". But what you notice at that moment is this: the score should be calculated by looking at frames, not at the moment of the roll. In the current design roll() calculates and score() only returns the result. Wrong place.
In Martin's slides this moment comes with the observation that responsibilities are in the wrong place: the design is wrong. The move commonly used in TDD practice at a moment like this is the real lesson of the kata: temporarily disable the new test, fix the design while the old tests are green, then turn the test back on. You don't refactor with a red test; the safety net of a refactoring is green tests.
it.skip('adds the next roll as a spare bonus', () => {
Test Files 1 passed (1)
Tests 2 passed | 1 skipped (3)
On green ground we turn roll() into something that stores the rolls and score() into something that walks frame by frame. Same behavior, two tests green. After removing skip and adding the spare rule to the frame loop, three tests are green. The strike test brings a surprise too:
AssertionError: expected NaN to be 24 // Object.is equality
The NaN comes from the frame loop advancing two rolls after a strike: at the end of the array it adds undefined. Again the red describes the missing behavior: a strike frame is a single roll. The code after adding the strike branch and refactoring:
export class BowlingGame {
private readonly rolls: number[] = [];
roll(pins: number): void {
this.rolls.push(pins);
}
score(): number {
let score = 0;
let frameStart = 0;
for (let frame = 0; frame < 10; frame++) {
if (this.isStrike(frameStart)) {
score += 10 + this.rolls[frameStart + 1] + this.rolls[frameStart + 2];
frameStart += 1;
} else if (this.isSpare(frameStart)) {
score += 10 + this.rolls[frameStart + 2];
frameStart += 2;
} else {
score += this.rolls[frameStart] + this.rolls[frameStart + 1];
frameStart += 2;
}
}
return score;
}
private isStrike(frameStart: number): boolean {
return this.rolls[frameStart] === 10;
}
private isSpare(frameStart: number): boolean {
return this.rolls[frameStart] + this.rolls[frameStart + 1] === 10;
}
}
The last test is the perfect game: twelve strikes, 300. Green on the first run, and this time that is expected: the test doesn't add behavior, it confirms our understanding.
✓ bowling-game.test.ts > BowlingGame > scores a gutter game as 0 1ms
✓ bowling-game.test.ts > BowlingGame > scores all ones as 20 0ms
✓ bowling-game.test.ts > BowlingGame > adds the next roll as a spare bonus 0ms
✓ bowling-game.test.ts > BowlingGame > adds the next two rolls as a strike bonus 0ms
✓ bowling-game.test.ts > BowlingGame > scores a perfect game as 300 0ms
Test Files 1 passed (1)
Tests 5 passed (5)
All five tests ran in 65 ms. That speed makes the kata a good ground for practicing TDD: the loop turns in seconds and attention always stays on the step itself.
Making the Kata Harder, and Catalogs
After you have solved a kata a few times the solution is memorized and the kata stops teaching anything. At that point you have to change the rules. Common variations:
- Keyboard only: no mouse. The fastest way to learn the IDE's refactoring shortcuts; you stop doing rename, extract method and inline by hand.
- No if: conditions are expressed through polymorphism, a lookup table or a data structure. In Bowling Game it forces you to model frame kinds as separate types.
- No primitives: numbers and strings are wrapped in small domain types. One of the Object Calisthenics rules from Jeff Bay's essay in The ThoughtWorks Anthology (2008); in String Calculator it gives birth to types like
DelimiterandNumbers. - A different starting point: once with the hardest item, once with the easiest. It lets you experience directly the "does order affect the design?" question from an earlier post.
- A different language or framework: solving the same kata in a language you don't know teaches you to tell the TDD rhythm apart from the language's new syntax.
The catalog on codingdojo.org lists more than fifty katas: FizzBuzz, Roman Numerals, Tennis, Mars Rover, Game of Life, Word Wrap and so on. Two criteria help when choosing: you should be able to explain the problem in ten minutes and write the first test in five. Some katas such as Gilded Rose are designed not for TDD from scratch but for getting existing code under test; we leave those to the upcoming Refactoring series.
In Production
The shape that works for bringing kata practice to a team is a small, regular session: once a week, 45–60 minutes, at a time that doesn't clash with code review or sprint meetings. A suggested flow: five minutes to choose the kata and that week's constraint, thirty minutes of randori or ping-pong pairs, ten minutes of retrospective. The retrospective is the most valuable part: questions like "where did our steps grow?", "which red surprised us?", "where did we skip refactoring?" make the habits in production code discussable as well.
For the first few sessions it is good for the facilitator to start with a prepared kata to show the team the rhythm. Then you move to randori. The code is not kept; it is deleted at the end of the session. Anyone who wants to keep it can keep it on their own branch, but the next session starts from an empty file again.
The benefit of katas is hard to measure, and trying to measure it can spoil it: a number of katas or "steps finished" metric pushes people to race through by rote. Better signals are indirect. You start to see tests and code arriving in the same commits, in small steps, in pull requests; "did you write this test first?" becomes a natural question in reviews; refactoring commits separate from behavior commits.
The rhythm learned in a kata doesn't carry over to production code as is. A kata has no dependencies, database or framework; real code does. So the real gain from a kata is not the technique itself but the skill of sensing step size and reading a red. In the next post we see what TDD looks like where there are dependencies, working from the outside in.
Common Pitfalls
Memorizing the kata solution and racing to the right answer. Writing the frame loop at the first test because you know the final shape of Bowling Game turns the kata into a typing exercise. If you didn't feel "the design is wrong" at the spare, you missed the kata's real lesson. Add a constraint the moment you notice you've memorized it.
Growing the kata like production code. Adding logging, dependency injection and a separator list from configuration to String Calculator kills the kata. Its value is in its smallness; as it grows, the loop slows and attention shifts from the step to the architecture.
Refactoring with a red test. If you try to change the design at the spare without skipping the test, you can't tell whether the red comes from the new test or from old behavior you broke.
Carrying on when time is up. Saying "I'll finish in two minutes" when the time box runs out puts finishing the kata ahead of step quality. Stop, do the retrospective, start from scratch next session.
A Small Experiment
★☆☆ Solve String Calculator again, this time passing the third test with fake it (parts[0] + parts[1]). Is the second step's test red this time? Which solution taught you more?
★★☆ Solve Bowling Game with the "no if" constraint. How did you model the frame kinds? Did your tests change, or only the implementation?
★★★ Organize a 45-minute randori with your team: the Roman Numerals kata, a five-minute time box, the last ten minutes for retrospective. In the retrospective, talk about who made the step bigger at which moment and how that affected the red.
In the next post we leave the sterile setting of katas: starting with an acceptance test that stays red for a real endpoint and building unit-level loops inside, we look at outside-in TDD, comparing the London and classical schools.