Testing the edges

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.

How to do it...

As we dig into this recipe, we will look for good boundaries to test against.

  1. Create a new file named recipe9.py in which to put all our code for this recipe.
  2. Pick a class to test. In this recipe, we'll use another variation of our Roman numeral converter. This one doesn't process values greater than 4000.
    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
  3. Create a test case that sets up an instance of the Roman numeral converter.
    import unittest
    
    class RomanNumeralTest(unittest.TestCase):
      def setUp(self):
        self.cvt = RomanNumeralConverter()
  4. Add several test methods that exercise the edges of converting to Roman numeral notation.
      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)
  5. Add several test methods that exercise the edges of converting to decimal notation.
      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")
  6. Add some tests that exercise the tiers of converting decimals to Roman numerals.
      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))
  7. Add some tests that input unexpected values to the Roman numeral converter.
      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)
  8. Add a unit test runner.
    if __name__ == "__main__":
      unittest.main()
  9. Run the test case.
    How to do it...

How it works...

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.

There's more...

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.

Identifying the edges is important

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.

Testing for unexpected conditions

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.197.251