Asserting the basics

The basic concept of an automated unittest test case is to instantiate part of our code, subject it to operations, and verify certain results using assertions.

  • If the results are as expected, unittest counts it as a test success
  • If the results don't match, an exception is thrown and unittest counts it as a test failure

Getting ready

Unittest was added to Python's standard batteries included library suite and doesn't require any extra installation.

How to do it...

With these steps, we will code a simple program and then write some automated tests using unittest:

  1. Create a new file called recipe1.py in which to put all of this recipe's code. Pick a class to test. This is known as the class under test. For this recipe, we'll pick a class that uses a simplistic Roman numeral converter:
    class RomanNumeralConverter(object):
      def __init__(self, roman_numeral):
        self.roman_numeral = roman_numeral
        self.digit_map = {"M":1000, "D":500,  "C":100, "L":50, "X":10, "V":5, "I":1}
    
      def convert_to_decimal(self):
        val = 0
        for char in self.roman_numeral:
          val += self.digit_map[char]
        return val

    Note

    This Roman numeral converter applies the simple rules of addition, but it doesn't have the special subtraction patterns such as XL mapping to 40. The purpose is not to have the best Roman numeral converter, but to observe the various test assertions.

  2. Write a new class and give it the same name with Test appended to the end, subclassing unittest.TestCase. Appending a test class with Test is a common convention, but not a requirement. Extending unittest.TestCase is a requirement needed to hook into unittest's standard test runner.
    import unittest
    
    class RomanNumeralConverterTest(unittest.TestCase):
  3. Create several methods whose names start with test, so they are automatically picked up by the test number of unittest.
      def test_parsing_millenia(self):
        value = RomanNumeralConverter("M")
        self.assertEquals(1000, value.convert_to_decimal())
    
      def test_parsing_century(self):
        value = RomanNumeralConverter("C")
        self.assertEquals(100, value.convert_to_decimal())
    
      def test_parsing_half_century(self):
        value = RomanNumeralConverter("L")
        self.assertEquals(50, value.convert_to_decimal())
    
      def test_parsing_decade(self):
        value = RomanNumeralConverter("X")
        self.assertEquals(10, value.convert_to_decimal())
    
      def test_parsing_half_decade(self):
        value = RomanNumeralConverter("V")
        self.assertEquals(5, value.convert_to_decimal())
    
      def test_parsing_one(self):
        value = RomanNumeralConverter("I")
        self.assertEquals(1, value.convert_to_decimal())
    
      def test_empty_roman_numeral(self):
        value = RomanNumeralConverter("")
        self.assertTrue(value.convert_to_decimal() == 0)
        self.assertFalse(value.convert_to_decimal() > 0)
    
      def test_no_roman_numeral(self):
        value = RomanNumeralConverter(None)
        self.assertRaises(TypeError, value.convert_to_decimal)
  4. Make the entire script runnable and then use unittest's test runner.
    if __name__ == "__main__":
      unittest.main()
  5. Run the file from the command line.
    How to do it...

How it works...

In the first step, we picked a class to test. Next, we created a separate test class. By naming the test class [class under test]Test, it is easy to tell which class is under test. Each test method name must start with test, so that unittest will automatically pick it up and run it. To add more tests, just define more test methods. Each of these tests utilizes various assertions.

  • assertEquals(first, second[, msg]): Compares first and second expressions; and fails, if they don't have the same value. We can optionally print a special message if there is a failure.
  • assertTrue(expression[, msg]): Tests the expression and fails if it is false. We can optionally print a special message if there is a failure.
  • assertFalse(expression[, msg]): Tests the expression and fails if it is true. We can optionally print a special message if there is a failure.
  • assertRaises(exception, callable, …): Runs the callable, with any arguments, for the callable listed afterwards, and fails if it doesn't raise the exception.

There's more...

Unittest provides many options for asserting, failing, and other convenient options. The following sections show some recommendations on how to pick and choose from these options.

assertEquals is preferred over assertTrue and assertFalse

When an assertEquals fails, the first and second values are printed in the error report, giving better feedback of what went wrong. assertTrue and assertFalse simply report failure. Not all testable results fit this but, if possible, use assertEquals.

It's important to understand the concept of equality. When comparing integers, strings, and other scalars, it's very simple. It doesn't work as well with collections like dictionaries, lists, and sets. Complex, custom-defined objects may carry custom definitions of equality. These complex objects may require more fine-grained assertions. That is why it's probably a good idea to also include some test methods that directly target equality and inequality when working with custom objects.

self.fail([msg]) can usually be rewritten with assertions

Unittest has a self.fail([msg]) operation that unconditionally causes the test to fail, along with an optional message. This was not shown earlier because it is not recommended for use.

The fail method is often used to detect certain situations like exceptions. A common idiom is as follows:

import unittest

class BadTest(unittest.TestCase):
  def test_no_roman_numeral(self):
    value = RomanNumeralConverter(None)
    try:
      value.convert_to_decimal()
      self.fail("Expected a TypeError")
    except TypeError, e:
      pass

This tests the same behavior as the earlier test_no_roman_numeral. The problem with this approach is that, when the code is working properly, the fail method is never executed. Code which is not executed regularly is at risk of becoming out of date and invalid. This will also interfere with coverage reports. Instead, it is better to use assertRaises as we used in the earlier examples. For other situations, look at rewriting the test using the other assertions.

Our version of Python can impact our options

Python's official documentation on unittest shows many other assertions, however, they depend on the version of Python we are using. Some have been deprecated; others are only available in later versions like Python 2.7.

If our code must support multiple versions of Python, then we must use the lowest common denominator. This recipe shows core assertions available in all versions since Python 2.1.

Note

A newer unittest2 (http://pypi.python.org/pypi/unittest2/) is under development that backports several of these newer unittest features into Python 2.4+. However, due to unittest2 being in the beta stage at the time of writing and limitations to the size of this book, I decided to focus on unittest.

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

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