Intro to property based tests
You can find all the code for this chapter here
Some companies will ask you to do the Roman Numeral Kata as part of the interview process. This chapter will show how you can tackle it with TDD.
We are going to write a function which converts an Arabic number (numbers 0 to 9) to a Roman Numeral.
If you haven't heard of Roman Numerals they are how the Romans wrote down numbers.
You build them by sticking symbols together and those symbols represent numbers
So I
is "one". III
is three.
Seems easy but there's a few interesting rules. V
means five, but IV
is 4 (not IIII
).
MCMLXXXIV
is 1984. That looks complicated and it's hard to imagine how we can write code to figure this out right from the start.
As this book stresses, a key skill for software developers is to try and identify "thin vertical slices" of useful functionality and then iterating. The TDD workflow helps facilitate iterative development.
So rather than 1984, let's start with 1.
Write the test first
If you've got this far in the book this is hopefully feeling very boring and routine to you. That's a good thing.
Try to run the test
Let the compiler guide the way
Write the minimal amount of code for the test to run and check the failing test output
Create our function but don't make the test pass yet, always make sure the tests fails how you expect
It should run now
Write enough code to make it pass
Refactor
Not much to refactor yet.
I know it feels weird just to hard-code the result but with TDD we want to stay out of "red" for as long as possible. It may feel like we haven't accomplished much but we've defined our API and got a test capturing one of our rules; even if the "real" code is pretty dumb.
Now use that uneasy feeling to write a new test to force us to write slightly less dumb code.
Write the test first
We can use subtests to nicely group our tests
Try to run the test
Not much surprise there
Write enough code to make it pass
Yup, it still feels like we're not actually tackling the problem. So we need to write more tests to drive us forward.
Refactor
We have some repetition in our tests. When you're testing something which feels like it's a matter of "given input X, we expect Y" you should probably use table based tests.
We can now easily add more cases without having to write any more test boilerplate.
Let's push on and go for 3
Write the test first
Add the following to our cases
Try to run the test
Write enough code to make it pass
Refactor
OK so I'm starting to not enjoy these if statements and if you look at the code hard enough you can see that we're building a string of I
based on the size of arabic
.
We "know" that for more complicated numbers we will be doing some kind of arithmetic and string concatenation.
Let's try a refactor with these thoughts in mind, it might not be suitable for the end solution but that's OK. We can always throw our code away and start afresh with the tests we have to guide us.
You may not have used strings.Builder
before
A Builder is used to efficiently build a string using Write methods. It minimizes memory copying.
Normally I wouldn't bother with such optimisations until I have an actual performance problem but the amount of code is not much larger than a "manual" appending on a string so we may as well use the faster approach.
The code looks better to me and describes the domain as we know it right now.
The Romans were into DRY too...
Things start getting more complicated now. The Romans in their wisdom thought repeating characters would become hard to read and count. So a rule with Roman Numerals is you can't have the same character repeated more than 3 times in a row.
Instead you take the next highest symbol and then "subtract" by putting a symbol to the left of it. Not all symbols can be used as subtractors; only I (1), X (10) and C (100).
For example 5
in Roman Numerals is V
. To create 4 you do not do IIII
, instead you do IV
.
Write the test first
Try to run the test
Write enough code to make it pass
Refactor
I don't "like" that we have broken our string building pattern and I want to carry on with it.
In order for 4 to "fit" with my current thinking I now count down from the Arabic number, adding symbols to our string as we progress. Not sure if this will work in the long run but let's see!
Let's make 5 work
Write the test first
Try to run the test
Write enough code to make it pass
Just copy the approach we did for 4
Refactor
Repetition in loops like this are usually a sign of an abstraction waiting to be called out. Short-circuiting loops can be an effective tool for readability but it could also be telling you something else.
We are looping over our Arabic number and if we hit certain symbols we are calling break
but what we are really doing is subtracting over i
in a ham-fisted manner.
Given the signals I'm reading from our code, driven from our tests of some very basic scenarios I can see that to build a Roman Numeral I need to subtract from
arabic
as I apply symbolsThe
for
loop no longer relies on ani
and instead we will keep building our string until we have subtracted enough symbols away fromarabic
.
I'm pretty sure this approach will be valid for 6 (VI), 7 (VII) and 8 (VIII) too. Nonetheless add the cases in to our test suite and check (I won't include the code for brevity, check the github for samples if you're unsure).
9 follows the same rule as 4 in that we should subtract I
from the representation of the following number. 10 is represented in Roman Numerals with X
; so therefore 9 should be IX
.
Write the test first
Try to run the test
Write enough code to make it pass
We should be able to adopt the same approach as before
Refactor
It feels like the code is still telling us there's a refactor somewhere but it's not totally obvious to me, so let's keep going.
I'll skip the code for this too, but add to your test cases a test for 10
which should be X
and make it pass before reading on.
Here are a few tests I added as I'm confident up to 39 our code should work
If you've ever done OO programming, you'll know that you should view switch
statements with a bit of suspicion. Usually you are capturing a concept or data inside some imperative code when in fact it could be captured in a class structure instead.
Go isn't strictly OO but that doesn't mean we ignore the lessons OO offers entirely (as much as some would like to tell you).
Our switch statement is describing some truths about Roman Numerals along with behaviour.
We can refactor this by decoupling the data from the behaviour.
This feels much better. We've declared some rules around the numerals as data rather than hidden in an algorithm and we can see how we just work through the Arabic number, trying to add symbols to our result if they fit.
Does this abstraction work for bigger numbers? Extend the test suite so it works for the Roman number for 50 which is L
.
Here are some test cases, try and make them pass.
Need help? You can see what symbols to add in this gist.
And the rest!
Here are the remaining symbols
100
C
500
D
1000
M
Take the same approach for the remaining symbols, it should just be a matter of adding data to both the tests and our array of symbols.
Does your code work for 1984
: MCMLXXXIV
?
Here is my final test suite
I removed
description
as I felt the data described enough of the information.I added a few other edge cases I found just to give me a little more confidence. With table based tests this is very cheap to do.
I didn't change the algorithm, all I had to do was update the allRomanNumerals
array.
Parsing Roman Numerals
We're not done yet. Next we're going to write a function that converts from a Roman Numeral to an int
Write the test first
We can re-use our test cases here with a little refactoring
Move the cases
variable outside of the test as a package variable in a var
block.
Notice I am using the slice functionality to just run one of the tests for now (cases[:1]
) as trying to make all of those tests pass all at once is too big a leap
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Add our new function definition
The test should now run and fail
Write enough code to make it pass
You know what to do
Next, change the slice index in our test to move to the next test case (e.g. cases[:2]
). Make it pass yourself with the dumbest code you can think of, continue writing dumb code (best book ever right?) for the third case too. Here's my dumb code.
Through the dumbness of real code that works we can start to see a pattern like before. We need to iterate through the input and build something, in this case a total.
Write the test first
Next we move to cases[:4]
(IV
) which now fails because it gets 2 back as that's the length of the string.
Write enough code to make it pass
It is basically the algorithm of ConvertToRoman(int)
implemented backwards. Here, we loop over the given roman numeral string:
We look for roman numeral symbols taken from
allRomanNumerals
, highest to lowest, at the beginning of the string.If we find the prefix, we add its value to
arabic
and trim the prefix.
At the end, we return the sum as the arabic number.
The HasPrefix(s, prefix)
checks whether string s
starts with prefix
and TrimPrefix(s, prefix)
removes the prefix
from s
, so we can proceed with the remaining roman numeral symbols. It works with IV
and all other test cases.
You can implement this as a recursive function, which is more elegant (in my opinion) but might be slower. I'll leave this up to you and some Benchmark...
tests.
Now that we have our functions to convert an arabic number into a roman numeral and back, we can take our tests a step further:
An intro to property based tests
There have been a few rules in the domain of Roman Numerals that we have worked with in this chapter
Can't have more than 3 consecutive symbols
Only I (1), X (10) and C (100) can be "subtractors"
Taking the result of
ConvertToRoman(N)
and passing it toConvertToArabic
should return usN
The tests we have written so far can be described as "example" based tests where we provide examples for the tooling to verify.
What if we could take these rules that we know about our domain and somehow exercise them against our code?
Property based tests help you do this by throwing random data at your code and verifying the rules you describe always hold true. A lot of people think property based tests are mainly about random data but they would be mistaken. The real challenge about property based tests is having a good understanding of your domain so you can write these properties.
Enough words, let's see some code
Rationale of property
Our first test will check that if we transform a number into Roman, when we use our other function to convert it back to a number that we get what we originally had.
Given random number (e.g
4
).Call
ConvertToRoman
with random number (should returnIV
if4
).Take the result of above and pass it to
ConvertToArabic
.The above should give us our original input (
4
).
This feels like a good test to build us confidence because it should break if there's a bug in either. The only way it could pass is if they have the same kind of bug; which isn't impossible but feels unlikely.
Technical explanation
We're using the testing/quick package from the standard library
Reading from the bottom, we provide quick.Check
a function that it will run against a number of random inputs, if the function returns false
it will be seen as failing the check.
Our assertion
function above takes a random number and runs our functions to test the property.
Run our test
Try running it; your computer may hang for a while, so kill it when you're bored :)
What's going on? Try adding the following to the assertion code.
You should see something like this:
Just running this very simple property has exposed a flaw in our implementation. We used int
as our input but:
You can't do negative numbers with Roman Numerals
Given our rule of a max of 3 consecutive symbols we can't represent a value greater than 3999 (well, kinda) and
int
has a much higher maximum value than 3999.
This is great! We've been forced to think more deeply about our domain which is a real strength of property based tests.
Clearly int
is not a great type. What if we tried something a little more appropriate?
Go has types for unsigned integers, which means they cannot be negative; so that rules out one class of bug in our code immediately. By adding 16, it means it is a 16 bit integer which can store a max of 65535
, which is still too big but gets us closer to what we need.
Try updating the code to use uint16
rather than int
. I updated assertion
in the test to give a bit more visibility.
Notice that now we are logging the input using the log
method from the testing framework. Make sure you run the go test
command with the flag -v
to print the additional output (go test -v
).
If you run the test they now actually run and you can see what is being tested. You can run multiple times to see our code stands up well to the various values! This gives me a lot of confidence that our code is working how we want.
The default number of runs quick.Check
performs is 100 but you can change that with a config.
Further work
Can you write property tests that check the other properties we described?
Can you think of a way of making it so it's impossible for someone to call our code with a number greater than 3999?
You could return an error
Or create a new type that cannot represent > 3999
What do you think is best?
Wrapping up
More TDD practice with iterative development
Did the thought of writing code that converts 1984 into MCMLXXXIV feel intimidating to you at first? It did to me and I've been writing software for quite a long time.
The trick, as always, is to get started with something simple and take small steps.
At no point in this process did we make any large leaps, do any huge refactorings, or get in a mess.
I can hear someone cynically saying "this is just a kata". I can't argue with that, but I still take this same approach for every project I work on. I never ship a big distributed system in my first step, I find the simplest thing the team could ship (usually a "Hello world" website) and then iterate on small bits of functionality in manageable chunks, just like how we did here.
The skill is knowing how to split work up, and that comes with practice and with some lovely TDD to help you on your way.
Property based tests
Built into the standard library
If you can think of ways to describe your domain rules in code, they are an excellent tool for giving you more confidence
Force you to think about your domain deeply
Potentially a nice complement to your test suite
Postscript
This book is reliant on valuable feedback from the community. Dave is an enormous help in practically every chapter. But he had a real rant about my use of 'Arabic numerals' in this chapter so, in the interests of full disclosure, here's what he said.
Just going to write up why a value of type
int
isn't really an 'arabic numeral'. This might be me being way too precise so I'll completely understand if you tell me to f off.A digit is a character used in the representation of numbers - from the Latin for 'finger', as we usually have ten of them. In the Arabic (also called Hindu-Arabic) number system there are ten of them. These Arabic digits are:
A numeral is the representation of a number using a collection of digits. An Arabic numeral is a number represented by Arabic digits in a base 10 positional number system. We say 'positional' because each digit has a different value based upon its position in the numeral. So
The
1
has a value of one thousand because its the first digit in a four digit numeral.Roman are built using a reduced number of digits (
I
,V
etc...) mainly as values to produce the numeral. There's a bit of positional stuff but it's mostlyI
always representing 'one'.So, given this, is
int
an 'Arabic number'? The idea of a number is not at all tied to its representation - we can see this if we ask ourselves what the correct representation of this number is:Yes, this is a trick question. They're all correct. They're the representation of the same number in the decimal, binary, English, hexadecimal and octal number systems respectively.
The representation of a number as a numeral is independent of its properties as a number - and we can see this when we look at integer literals in Go:
And how we can print integers in a format string:
We can write the same integer both as a hexadecimal and an Arabic (decimal) numeral.
So when the function signature looks like
ConvertToRoman(arabic int) string
it's making a bit of an assumption about how it's being called. Because sometimesarabic
will be written as a decimal integer literalBut it could just as well be written
Really, we're not 'converting' from an Arabic numeral at all, we're 'printing' - representing - an
int
as a Roman numeral - andint
s are not numerals, Arabic or otherwise; they're just numbers. TheConvertToRoman
function is more likestrconv.Itoa
in that it's turning anint
into astring
.But every other version of the kata doesn't care about this distinction so :shrug:
Last updated