Testing corner cases by iteration

While developing code, new corner case inputs are often discovered. Being able to capture these inputs in an iterable array makes it easy to add related test methods.

How to do it...

In this recipe, we will look at a different way to test corner cases.

  1. Create a new file called recipe10.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 class to exercise the Roman numeral converter.
    import unittest
    
    class RomanNumeralTest(unittest.TestCase):
      def setUp(self):
        self.cvt = RomanNumeralConverter()
  4. Write a test method that exercises the edges of the Roman numeral converter.
      def test_edges(self):
        r = self.cvt.convert_to_roman
        d = self.cvt.convert_to_decimal
        edges = [("equals", r, "I", 1),
             ("equals", r, "", 0),
             ("equals", r, "", -1),
             ("equals", r, "MMMM", 4000),
             ("raises", r, Exception, 4001),
             ("equals", d, 1, "I"),
             ("equals", d, 0, ""),
             ("equals", d, 4000, "MMMM"),
             ("raises", d, Exception, "MMMMI")
            ]
    
        [self.checkout_edge(edge) for edge in edges]
  5. Create a test method that exercises the tiers converting from decimal to Roman numerals.
      def test_tiers(self):
        r = self.cvt.convert_to_roman
        edges = [("equals", r, "V", 5),
             ("equals", r, "VIIII", 9),
             ("equals", r, "X", 10),
             ("equals", r, "XI", 11),
             ("equals", r, "XXXXVIIII", 49),
             ("equals", r, "L", 50),
             ("equals", r, "LI", 51),
             ("equals", r, "LXXXXVIIII", 99),
             ("equals", r, "C", 100),
             ("equals", r, "CI", 101),
             ("equals", r, "CCCCLXXXXVIIII", 499),
             ("equals", r, "D", 500),
             ("equals", r, "DI", 501),
             ("equals", r, "M", 1000)
            ]
    
        [self.checkout_edge(edge) for edge in edges]
  6. Create a test method that exercises a set of invalid inputs.
      def test_bad_inputs(self):
        r = self.cvt.convert_to_roman
        d = self.cvt.convert_to_decimal
        edges = [("equals", r, "", None),
             ("equals", r, "I", 1.2),
             ("raises", d, TypeError, None),
             ("raises", d, TypeError, 1.2)
            ]
    
        [self.checkout_edge(edge) for edge in edges]
  7. Code a utility method that iterates over the edge cases and runs different assertions based on each edge.
      def checkout_edge(self, edge):
        if edge[0] == "equals":
          f, output, input = edge[1], edge[2], edge[3]
          print("Converting %s to %s..." % (input, output))
          self.assertEquals(output, f(input))
        elif edge[0] == "raises":
          f, exception, args = edge[1], edge[2], edge[3:]
          print("Converting %s, expecting %s" % 
                          (args, exception))
          self.assertRaises(exception, f, *args)
  8. Make the script runnable by loading the test case into TextTestRunner.
    if __name__ == "__main__":
      suite = unittest.TestLoader().loadTestsFromTestCase( 
                     RomanNumeralTest)
      unittest.TextTestRunner(verbosity=2).run(suite)
  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. The immediate edges which 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.

But we've written the tests a little differently. Instead of writing each test input/output combination as a separate test method, we capture the input and output values in a tuple that is embedded in a list. We then feed it to our test iterator, checkout_edge. Because we need both assertEquals and assertRaises calls, the tuple also includes either equals or raises to flag which assertion to use.

Finally, to make it flexibly handle the convertion of both Roman numerals and decimals, the handles on the convert_to_roman and convert_to_decimal functions of our Roman numeral API is embedded in each tuple as well.

As shown in the following highlighted parts, we grab a handle on convert_to_roman, and store it in r. Then we embed it in the third element of the highlighted tuple, allowing the checkout_edge function to call it when needed.

  def test_bad_inputs(self):
    r = self.cvt.convert_to_roman
    d = self.cvt.convert_to_decimal
    edges = [("equals", r, "", None),
         ("equals", r, "I", 1.2),
         ("raises", d, TypeError, None),
         ("raises", d, TypeError, 1.2)
        ]

    [self.checkout_edge(edge) for edge in edges]

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 a separate test method that has a list of input/output values to check those out as well. In the recipe Testing the edges, we didn't include testing before and after these mini-edges, for example 4 and 6 for 5. Now that it only takes one line of data to capture this test, we have it in this recipe. The same was done for all the others (except 1000).

Finally, we need to check bad inputs, so we created one more test method where we try to convert None and a float to and from Roman numeral.

Does this defy the recipe—Breaking down obscure tests into simple ones?

In a way, it does. If something goes wrong in one of the test data entries, then that entire test method will have failed. That is one reason why the other recipe split things up into three test methods instead of one big test method to cover them all. This is a judgment call about when it makes sense to view inputs and outputs as more data than test method. If you find the same sequence of test steps occurring repeatedly, consider whether it makes sense to capture the values in some sort of table structure, like the list used in this recipe.

How does this compare with the recipe—Testing the edges?

In case it wasn't obvious, these are the exact same tests used in the recipe Testing the edges. The question is, which version do you find more readable? Both are perfectly acceptable. Breaking things up into separate methods makes it more fine-grained and easier to spot if something goes wrong. Collecting things together into a data structure, the way we did in this recipe makes it more succinct, and could spur us on to write more test combinations as we did for the conversion tiers.

In my opinion, when testing algorithmic functions that have simple inputs and outputs, it's more suitable to use this recipe's mechanism to code an entire battery of test inputs in this concise format, for example, a mathematical function, a sorting algorithm, or perhaps a transform function.

When testing functions that are more logical and imperative, the other recipe may be more useful. For example, functions that interact with a database, cause changes in the state of the system, or other types of side effects that aren't encapsulated in the return value, would be hard to capture using this recipe.

See also

  • Breaking down obscure tests into simple ones
  • Testing the edges

2

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

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