This is my attempt at the code kata for converting roman numerals into their respective number. It may seem familiar to a well known kata, but this is actually in reverse because the kata that is often used is the other direction: a number to roman numerals. I’ve decided to reverse it since theres already a million articles on how to do it the first way, and doing it in reverse provides more opportunity for potential errors that are normally left by the wayside; which is the real focus of this article.

So let’s break it down, it’s a pretty simple set of requirements:

Given any roman numerals that represent 1 to 1000, return the number.

One requirement!? Lets’s do this!

Focus on the Bad, First

As I said before, when we get to the end of the requirements, we feel the task is finished. It’s easy to sit back in your chair and relax and feed quietly proud. There’s nothing wrong with that. In fact, if you don’t feel good about your solution then that’s a strong indication something is fundamentally wrong.

TDD forces us to write tests along the way so that when we do get to that last test we truly are at the end (or at least closest to truly finished if followed correctly). Unfortunately, this leads to some oversight in that we expect the user to use the software in the way the requirements are provided. This almost certainly is never the case. Which is why that one person you showed it to went for the very thing that didn’t have a requirement dictating the behaviour that should be seen.

Always focus, and write tests for all the misuses, edge cases, etc of the solution before you write the successful cases. You will not be able to handle them all at the start, but you should try and get as many out of the way as possible right now. For example, here is a list of things that may go wrong with the roman numeral calculator:

Lower case is allowed? For this yes, xviis the same as XVI.

Invalid characters, like a P.

A blank string is ambiguous, do they mean zero or is this an error? We want this to be an error.

Invalid input type is if we are handed another value other than a string to convert. Especially important in loosely typed languages like Python.

Invalid range are values of roman numerals that translate to a value that is greater than the allowed 1000.

Surely if somebody gives us a string that’s too long, say 500 characters we should not attempt to process it. We will limit the input to 25 characters even though thats way more than we actually need.

Valid roman numerals that are given to us in an improper syntax, like IIIIIV.

Seeing all the potential issues we can now decide and clarify on:

The requirements to do not give us enough information to resolve some of the ambiguities. This can lead to exponentially more work and complexity the longer they are left. Impossible or conflicting requirements under edge cases. Anything else we may not have discovered by just coding the golden path.

We won’t be able to hammer out all of these initially, but we should try and do as many as possible first. The most important thing is that we are aware of them, and we know the solution is not complete until they are all ticked off by the end.

For brevity, I will not be showing the solutions to every single test. Rest assured that TDD is happening behind the scenes, but the article would get very long. Handling as many of the bad cases we outlined above I am so far up to:

import unittest class RomanToNumberConverter:

def validate(self, roman):

if type(roman) is not str:

raise ValueError("You must provide a string.")

if roman == '':

raise ValueError("An empty string was provided.")

if len(roman) > 25:

raise ValueError("Input string is over 25 characters.") raise ValueError("Invalid roman numerals: " + roman.upper()) def roman_to_number(self, roman):

self.validate(roman) class TestRomanToNumber(unittest.TestCase):

def assertError(self, msg, *args, **kwargs):

try:

converter = RomanToNumberConverter()

converter.roman_to_number(*args, **kwargs)

self.assertFail()

except ValueError as e:

self.assertEqual(e.message, msg) def test_P_is_invalid(self):

self.assertError('Invalid roman numerals: P', 'P') def test_blank_string(self):

self.assertError('An empty string was provided.', '') def test_invalid_type(self):

self.assertError('You must provide a string.', 123) def test_string_too_long(self):

self.assertError('Input string is over 25 characters.', 'I' * 26) def test_Z_is_invalid(self):

self.assertError('Invalid roman numerals: Z', 'Z') def test_always_convert_to_upper_case(self):

self.assertError('Invalid roman numerals: U', 'u') # This will run the unit tests

unittest.main()

Handling the Regular Conditions

Now we can continue with the original requirements in the same TDD fashion. I will only highlight the changes for each test. The commented out lines provide context for the modified classes/functions.