When we write automated tests, we pick the inputs and assert the expected outputs. It is important to test the limits of the inputs to make sure our code can handle good and bad inputs. This is also known as testing corner cases.
As we dig into this recipe, we will look for good boundaries to test against.
recipe9.py
in which to put all our code for this recipe.class RomanNumeralConverter(object): def __init__(self): self.digit_map = {"M":1000, "D":500, "C":100, "L":50, "X":10, "V":5, "I":1} def convert_to_decimal(self, roman_numeral): val = 0 for char in roman_numeral: val += self.digit_map[char] if val > 4000: raise Exception("We don't handle values over 4000") return val def convert_to_roman(self, decimal): if decimal > 4000: raise Exception("We don't handle values over 4000") val = "" mappers = [(1000,"M"), (500,"D"), (100,"C"), (50,"L"), (10,"X"), (5,"V"), (1,"I")] for (mapper_dec, mapper_rom) in mappers: while decimal >= mapper_dec: val += mapper_rom decimal -= mapper_dec return val
import unittest class RomanNumeralTest(unittest.TestCase): def setUp(self): self.cvt = RomanNumeralConverter()
def test_to_roman_bottom(self): self.assertEquals("I", self.cvt.convert_to_roman(1)) def test_to_roman_below_bottom(self): self.assertEquals("", self.cvt.convert_to_roman(0)) def test_to_roman_negative_value(self): self.assertEquals("", self.cvt.convert_to_roman(-1)) def test_to_roman_top(self): self.assertEquals("MMMM", self.cvt.convert_to_roman(4000)) def test_to_roman_above_top(self): self.assertRaises(Exception, self.cvt.convert_to_roman, 4001)
def test_to_decimal_bottom(self): self.assertEquals(1, self.cvt.convert_to_decimal("I")) def test_to_decimal_below_bottom(self): self.assertEquals(0, self.cvt.convert_to_decimal("")) def test_to_decimal_top(self): self.assertEquals(4000, self.cvt.convert_to_decimal("MMMM")) def test_to_decimal_above_top(self): self.assertRaises(Exception, self.cvt.convert_to_decimal, "MMMMI")
def test_to_roman_tier1(self): self.assertEquals("V", self.cvt.convert_to_roman(5)) def test_to_roman_tier2(self): self.assertEquals("X", self.cvt.convert_to_roman(10)) def test_to_roman_tier3(self): self.assertEquals("L", self.cvt.convert_to_roman(50)) def test_to_roman_tier4(self): self.assertEquals("C", self.cvt.convert_to_roman(100)) def test_to_roman_tier5(self): self.assertEquals("D", self.cvt.convert_to_roman(500)) def test_to_roman_tier6(self): self.assertEquals("M", self.cvt.convert_to_roman(1000))
def test_to_roman_bad_inputs(self): self.assertEquals("", self.cvt.convert_to_roman(None)) self.assertEquals("I", self.cvt.convert_to_roman(1.2)) def test_to_decimal_bad_inputs(self): self.assertRaises(TypeError, self.cvt.convert_to_decimal, None) self.assertRaises(TypeError, self.cvt.convert_to_decimal, 1.2)
if __name__ == "__main__": unittest.main()
We have a specialized Roman numeral converter that only converts values up to MMMM
or 4000
. We have written several test methods to exercise it. The immediate edges we write tests for are 1
and 4000
. We also write some tests for one step past that: 0
and 4001
. To make things complete, we also test against -1
.
A key part of the algorithm involves handling the various tiers of Roman numerals (5, 10, 50, 100, 500, and 1000). These could be considered mini-edges, so we wrote tests to check that the code handled those as well. Do you think we should test one past the mini-edges?
It's recommended that we should. Many bugs erupt due to coding greater than, when it should be greater than or equal (or vice versa), and so on. Testing one past the boundary, in both directions, is the perfect way to make sure that things are working exactly as expected. We also need to check bad inputs, so we tried converting None
and a float
.
That previous statement raises an important question: how many invalid types should we test against? Because Python is dynamic, we can expect a lot of input types. So what is reasonable? If our code hinges on a dictionary lookup, like certain parts of our Roman numeral API does, then confirming that we correctly handle a KeyError would probably be adequate. We don't need to input lots of different types if they all result in a KeyError.
It's important to identify the edges of our system, because we need to know our software can handle these boundaries. We also need to know it can handle both sides of these boundaries that are good values and bad values. That is why we need to check 4000
and 4001
, as well as 0
and 1
. This is a common place where software breaks.
Does this sound a little awkward? Expect the unexpected? Our code involves converting integers and strings back and forth. By 'unexpected', we mean types of inputs passed in when someone uses our library that doesn't understand the edges, or wires it to receive inputs that are wider ranging types than we expected to receive.
A common occurrence of misuse is when a user of our API is working against a collection such as a list and accidentally passes the entire list instead of a single value by iteration. Another often seen situation is when a user of our API passes in None
due to some other bug in their code. It's good to know that our API is resilient enough to handle this.
18.119.103.204