07/01/2023
13 min read
TDD

Content:

Introduction to Test Driven Development with Roman Numerals



What is Test driven development (alias TDD) ?


Contrary to what a lot of people think it’s not a testing strategy but a way of solving problems and design your production code in an incremental way by a series of short validated hypothesis.

The tests made in the cycle are only a side effects of the practice and allow us to refactor safely. Refactoring is what make TDD worth it because with time in every projects code rots and the ability

To refactor can help us to fight against that.

TDD is defined by a 3 steps cycle RED, GREEN and REFACTOR.


RED:

We make a failing test (not compiling is considered as a failure too).


GREEN:

We make the production code that make the failing test pass in the simplest way possible (without adding more code that we need to to make the test pass).


REFACTOR:

That’s when we focus on the design part and remove bad duplications because we can’t usually make something works and pretty at the same time.



Presentation of the Roman Numerals kata


Create a function that takes a Roman numeral as its argument and returns its value as a numeric decimal integer. You don't need to validate the form of the Roman numeral.

Modern Roman numerals are written by expressing each decimal digit of the number to be encoded separately, starting with the leftmost digit and skipping any 0s.

So 1990 is rendered "MCMXC" (1000 = M, 900 = CM, 90 = XC) and 2008 is rendered "MMVIII" (2000 = MM, 8 = VIII). The Roman numeral for 1666, "MDCLXVI", uses each letter in descending order.


Exemple:


1 romanNumerals(M); // should return 1000


Help:



[object Object]


Solving the Roman Numerals Kata with TDD


For solving this problem i’ll use typescript and jest for my testing framework.

Let’s start with simple test possible which ensure that when we pass an empty string the value that we get is zero, this test will help us to put our pubic API in place.


1 2 3 4 5 describe("roman numerals", () => { it("should be 0 when we pass an empty string", () => { expect(romanNumerals("")).toBe(0); }); });


We make it pass in the simplest way possible .


1 export const romanNumerals = (romanNumber: string) => 0;


Next test we will convert I into 1 pretty simple isn’t it ?


1 2 3 it("should be 1 when we pass the roman number I", () => { expect(romanNumerals("I")).toBe(1); });


No surprise the test fails, we handle that case with a simple if statement.


1 2 3 4 5 export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; return 1; };


It seems dumb I know but hold on till the end, the more we will advance in the kata resolution the more it will start to make sense.

Next test we will convert V into 5.


1 2 3 4 it("should be 5 when we pass the roman number V", () => { expect(romanNumerals("V")).toBe(5); });


Another if statement to make the test pass.


1 2 3 4 5 6 7 export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; if(romanNumber === 'V') return 5 return 1; };


We do the same for 10 , hold on why not doing 2 ? Because it’s too complex we have to handle two characters (II) it’s already too

complex and general we want to go from the least complex to the most complex, the most specific to the most generic.


1 2 3 it("should be 10 when we pass the roman number X", () => { expect(romanNumerals("X")).toBe(10); });


We make it pass with the same dumb technique.


1 2 3 4 5 6 7 8 9 export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; if (romanNumber === "V") return 5; if(romanNumber === "X") return 10; return 1; };


Do you see the duplication ? Now we can pass in refactor mode . This technique to make pattern emerge with puting duplication in evidence is called triangulation.


1 2 3 4 5 6 7 8 9 10 11 const romanToArabic: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; return romanToArabic[romanNumber]; };


Great we got rid of our ugly if statement with this simple mapping trick.

We can now make the concept of handling number with multiple characters by writing a test that assert that we can successfully convert II to 2.


1 2 3 it("should be 2 when we pass the roman number II", () => { expect(romanNumerals("II")).toBe(2); });


For now we can make it pass with another if statement.


1 2 3 4 5 6 7 8 9 10 11 12 13 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; if (romanNumber === "II") return 2; return romanToArabic[romanNumber]; };


Hey why not make a for loop right away ? Because on non trivial problem I prefer to write looping logic at the end when

All the concepts of the problem has been discovered.

Next test we convert III to 3.


1 2 3 it("should be 3 when we pass the roman number III", () => { expect(romanNumerals("III")).toBe(3); });


We can make it pass easily like this.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { if (romanNumber.length === 0) return 0; let arabianNumber = 0; if (romanNumber.length > 0) { arabianNumber += romanToArabic[romanNumber[0]]; } if (romanNumber.length > 1) { arabianNumber += romanToArabic[romanNumber[1]]; } if (romanNumber.length > 2) { arabianNumber += romanToArabic[romanNumber[2]]; } return arabianNumber; };


We just increment a number base on each characters in the Roman number, we can still do better by removing the first if statement

which is now useless and replace the hardcoded index by a variable that we increments in each condition.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { let arabianNumber = 0; let i = 0; if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } return arabianNumber; };


It looks a lot like a while loop isn’t it ? But we have still one concept to handle before writing the looping logic.

What happened when we try to convert IV to 4 ?


1 2 3 it("should be 4 when we pass the roman number IV", () => { expect(romanNumerals("IV")).toBe(4); });


The tests fails we received 6 instead of 4, looking at our actual production code it make sense because we just convert each Roman number to Arabian number.

We can make it pass easily with this simple modification.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { let arabianNumber = 0; let i = 0; if (romanNumber.length > i) { if (romanNumber[i] === "I" && romanNumber[i + 1] === "V") { arabianNumber -= romanToArabian[romanNumber[i]]; } else { arabianNumber += romanToArabian[romanNumber[i]]; } i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } return arabianNumber; };


It works but the solution is not generic enough yet, to make it more generic we can go further and try to convert IX to 9.

What IV who represent 4 , IX who represent 9, XL who represent 40 have in common ? It ’s kind of obvious all this specific roman number have their first character

smaller than the next character hence by subtracting the next number by the smaller character we have the good result 5 - 1 = 4, 10 -1 = 9, 50 - 10 = 40 etc…

Let’s make a test to verify our hypothesis.


1 2 3 it("should be 9 when we pass the roman number IX", () => { expect(romanNumerals("IX")).toBe(9); });


Of course the test fail our production code is still too specific.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, }; export const romanNumerals = (romanNumber: string) => { let arabianNumber = 0; let i = 0; if (romanNumber.length > i) { if (romanToArabian[romanNumber[i]] && romanToArabian[romanNumber[i + 1]]) { arabianNumber -= romanToArabian[romanNumber[i]]; } else { arabianNumber += romanToArabian[romanNumber[i]]; } i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } if (romanNumber.length > i) { arabianNumber += romanToArabian[romanNumber[i]]; i++; } return arabianNumber; };


We just maked a tiny modification to make the test pass hence we verify our previous hypothesis.

The kata is now mostly solved . We just need to had a last test that force us to write the looping logic and add the other numbers and the kata will be done.

Let’s write this test.


1 2 3 it("should be 1666 when we pass the roman number MDCLXVI", () => { expect(romanNumerals("MDCLXVI")).toBe(1666); });


It fails to make it pass we just have to write the looping logic and complete the mapping which is trivial.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000, }; export const romanNumerals = (romanNumber: string) => { let arabianNumber = 0; let i = 0; while (romanNumber.length > i) { if (romanToArabian[romanNumber[i]] && romanToArabian[romanNumber[i + 1]]) { arabianNumber -= romanToArabian[romanNumber[i]]; } else { arabianNumber += romanToArabian[romanNumber[i]]; } i++; } return arabianNumber; };


The kata is now done but we can still improve the code by transforming the while loop into a for loop.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000, }; export const romanNumerals = (romanNumber: string) => { let arabianNumber = 0; for (let i = 0; romanNumber.length > i; i++) { if (romanToArabian[romanNumber[i]] && romanToArabian[romanNumber[i + 1]]) { arabianNumber -= romanToArabian[romanNumber[i]]; } else { arabianNumber += romanToArabian[romanNumber[i]]; } } return arabianNumber; };


I tend to prefer code written in a functional way so my final solution look like this.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const romanToArabian: Record<string, number> = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000, }; export const romanNumerals = (romanNumber: string) => romanNumber .split("") .reduce( (acc, curr, i) => romanToArabian[curr] && romanToArabian[romanNumber[i + 1]] ? acc - romanToArabian[curr] : acc + romanToArabian[curr], 0 );


Conclusion


By solving this kata with TDD we explored some concepts of it:


Baby steps:


The art of cut a problem in many small problems and taking care of each one separately.


Refactoring in a safe way:


Restructuring existing code without changing its behavior.


I know real projects with all their depedencies are much more complicated than that so we will dive into that in another article.

You should make yourself comfortable with TDD with some katas first and then some side projects before using it at work.

If you are curious to see how a real project built with TDD (and  hexagonal architecture) frontend and backend you can look at these repositories: