TypeScript Test Driven Development

7th February 2021

Karate Coding

I recently completed a Kata involving converting arabic integers - our whole number system, to Roman Numerals in TypeScript. Any regular reader will know I go by a mantra of, when we're doing something unimportant, we should be creating learning opportunities. So I took the opportunity to exercise a strict Test Driven Development approach.

Test Driven Development (TDD) is a programming style known for its test-first approach, where testing, design and programming are tightly bound together, but essentially driven by rules specified in Unit Tests. TDD is widely reported to drive better software design, reduce coding errors and actually reduce development effort under agile development.

You can see the code for this project on my github, look for the details in the README.md file for how to setup. Look at the git commits to follow along with the TDD approach.

So what's TDD look like for a TypeScript application?

For this project I chose to use Jest for testing my application. Jest is a very popular Javascript testing framework, and for our purposes a simple test takes an input and asserts an expected output like this:

test('converts decimal 1 to roman numeral I', () => {
    const result: string = convertNumerals(1);
    const expected: string = 'I';

    expect(result).toBe(expected);
});

Just as the test string say, given the input 1 we expect the Roman Numeral I; converts decimal 1 to roman numeral I

This clearly explains the expected behaviour, and given spells out what is expected in terms of input to output.

The Challenge of TDD

Test Driven Development challenges developers to not think too far ahead, and instead focus on incremental behavioural changes. A common mistake is to sit in the tests writing all possible scenarios. Where this might seem like a good idea, this sort of test driven development removes the potential for improved software design from our test/code/design triade.

Instead we focus on incremental tests. This in reality likely means our first code might be poorer than where we may have started by jumping ahead, but by focusing on incremental changes driven by incremental tests.

How this helped build a better application

Initially I went along the lines of a very simple approach. I knew at this point that going forward this was not going to work. This didn't matter, because we're following TDD:

export const convertNumerals = (givenInt: number) : string => {

    for (let i=0; i<givenInt; i++) {
        romanNumeral += 'I';
    }

    return romanNumeral;
}

This simply looped and returned I to the number of times of the given integer. This works for numbers 1-3. However when behaviour changes for integer 4 we get result IIIII not the expected IV.

See commit

This is where the given test assertion helped us determine when a code change driven by the test should take place;

export const convertNumerals = (givenInt: number) : string => {
    let romanNumeral: string = '';
    let remainValue: number = givenInt;

    if (remainValue == 4) {
        romanNumeral += 'IV'
        remainValue -= 4;
    }

    if (remainValue >= 5) {
        romanNumeral += 'V'
        remainValue -= 5;
    }

    while (remainValue > 0) {
        romanNumeral += 'I';
        remainValue -= 1;
    }

    return romanNumeral;
}

See commit

This worked for numbers up to 5, but once behaviour changed with number 6, this failed. Ultimately through iterations, and surprisingly to me - very little cognitive effort I stumbled across this solution after observing patterns in the unit tests;

export const convertNumerals = (givenInt: number) : string => {
    let romanNumeral: string = '';
    let remainValue: number = givenInt;

    const intToRom: Array<intRomDef> = [
        {int:10, rom:'X'}, {int:9, rom:'IX'}, {int:5, rom:'V'}, {int:4, rom:'IV'}, {int:1, rom:'I'}, 
    ];

    intToRom.forEach(intRom => {
        while (remainValue > intRom.int-1) {
            romanNumeral += intRom.rom;
            remainValue -= intRom.int;
        }        
    });

    return romanNumeral;
}

Essentially, I kept track of a "remaining" value. Going from larger number to smallest, each time we found a Roman Numeral which fit inside the remainder, we appended it to our Roman Numeral string, and deducted it from our remainder. This is of course much more efficient than our ever growing if statement conditions.

See commit

The point is this; by letting tests drive application behaviour we improved application design.

Where to go from here

This application's design uses patterns of such as IV as a given glyph. There is a pattern that emerges as more Roman Numerals were added, and it's evident this is not an efficient mechanism for comparing and converting. This Kata was completed in just over an hour, and achieving further efficiency wasn't drawn out in that time. I would suspect by continuing this Kata better application design would emerge from the TDD.

Want to have a go yourself? Why not checkout this website, you'll also be able to read through other people's solutions there; https://codingdojo.org/kata/RomanNumerals/