Dive Into Python Python for experienced programmers |

## 6.6. `roman.py`, stage 1

Now that our unit test is complete, it's time to start writing the code that our test cases are attempting to test. We're going to do this in stages, so we can see all the unit tests fail, then watch them pass one by one as we fill in the gaps in `roman.py`.

Example 6.7. `roman1.py`

If you have not already done so, you can download this and other examples used in this book.

    """Convert to and from Roman numerals"""

    #Define exceptions
    class RomanError(Exception): pass
    class OutOfRangeError(RomanError): pass
    class NotIntegerError(RomanError): pass
    class InvalidRomanNumeralError(RomanError): pass

    def toRoman(n):
        """convert integer to Roman numeral"""
        pass

    def fromRoman(s):
        """convert Roman numeral to integer"""
        pass

| This is how you define your own custom exceptions in Python. Exceptions are classes, and you create your own by subclassing existing exceptions. It is strongly recommended (but not required) that you subclass `Exception`, which is the base class that all built-in exceptions inherit from. Here I am defining `RomanError` (inherited from `Exception`) to act as the base class for all my other custom exceptions to follow. This is a matter of style; I could just as easily have inherited each individual exception from the `Exception` class directly.
| The `OutOfRangeError` and `NotIntegerError` exceptions will eventually be used by `toRoman` to flag various forms of invalid input, as specified in `ToRomanBadInput`.
| The `InvalidRomanNumeralError` exception will eventually be used by `fromRoman` to flag invalid input, as specified in `FromRomanBadInput`.
| At this stage, we want to define the API of each of our functions, but we don't want to code them yet, so we stub them out using the Python reserved word `pass`.

Now for the big moment (drum roll please): we're finally going to run our unit test against this stubby little module. At this point, every test case should fail. In fact, if any test case passes in stage 1, we should go back to `romantest.py` and re-evaluate why we coded a test so useless that it passes with do-nothing functions.

Run `romantest1.py` with the `-v` command-line option, which will give more verbose output so we can see exactly what's going on as each test case runs. With any luck, your output should look like this:

Example 6.8. Output of `romantest1.py` against `roman1.py`

    fromRoman should only accept uppercase input ... ERROR
    toRoman should always return uppercase ... ERROR
    fromRoman should fail with malformed antecedents ... FAIL
    fromRoman should fail with repeated pairs of numerals ... FAIL
    fromRoman should fail with too many repeated numerals ... FAIL
    fromRoman should give known result with known input ... FAIL
    toRoman should give known result with known input ... FAIL
    fromRoman(toRoman(n))==n for all n ... FAIL
    toRoman should fail with non-integer input ... FAIL
    toRoman should fail with negative input ... FAIL
    toRoman should fail with large input ... FAIL
    toRoman should fail with 0 input ... FAIL

    ======================================================================
    ERROR: fromRoman should only accept uppercase input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
        roman1.fromRoman(numeral.upper())
    AttributeError: 'None' object has no attribute 'upper'
    ======================================================================
    ERROR: toRoman should always return uppercase
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
        self.assertEqual(numeral, numeral.upper())
    AttributeError: 'None' object has no attribute 'upper'
    ======================================================================
    FAIL: fromRoman should fail with malformed antecedents
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
        self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: InvalidRomanNumeralError
    ======================================================================
    FAIL: fromRoman should fail with repeated pairs of numerals
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs
        self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: InvalidRomanNumeralError
    ======================================================================
    FAIL: fromRoman should fail with too many repeated numerals
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
        self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: InvalidRomanNumeralError
    ======================================================================
    FAIL: fromRoman should give known result with known input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues
        self.assertEqual(integer, result)
      File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
        raise self.failureException, (msg or '%s != %s' % (first, second))
    AssertionError: 1 != None
    ======================================================================
    FAIL: toRoman should give known result with known input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
        self.assertEqual(numeral, result)
      File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
        raise self.failureException, (msg or '%s != %s' % (first, second))
    AssertionError: I != None
    ======================================================================
    FAIL: fromRoman(toRoman(n))==n for all n
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity
        self.assertEqual(integer, result)
      File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
        raise self.failureException, (msg or '%s != %s' % (first, second))
    AssertionError: 1 != None
    ======================================================================
    FAIL: toRoman should fail with non-integer input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testDecimal
        self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: NotIntegerError
    ======================================================================
    FAIL: toRoman should fail with negative input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
        self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: OutOfRangeError
    ======================================================================
    FAIL: toRoman should fail with large input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
        self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: OutOfRangeError
    ======================================================================
    FAIL: toRoman should fail with 0 input
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
        self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
      File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
        raise self.failureException, excName
    AssertionError: OutOfRangeError
    ----------------------------------------------------------------------
    Ran 12 tests in 0.040s

    FAILED (failures=10, errors=2)

| Running the script runs `unittest.main()`, which runs each test case, which is to say each method defined in each class within `romantest.py`. For each test case, it prints out the `doc string` of the method and whether that test passed or failed. As expected, none of our test cases passed.
| For each failed test case, `unittest` displays the trace information showing exactly what happened. In this case, our call to `assertRaises` (also called `failUnlessRaises`) raised an `AssertionError` because it was expecting `toRoman` to raise an `OutOfRangeError` and it didn't.
| After the detail, `unittest` displays a summary of how many tests were performed and how long it took.
| Overall, the unit test failed because at least one test case did not pass. When a test case doesn't pass, `unittest` distinguishes between failures and errors. A failure is a call to an `assertXYZ` method, like `assertEqual` or `assertRaises`, that fails because the asserted condition is not true or the expected exception was not raised. An error is any other sort of exception raised in the code we're testing or the unit test case itself. For instance, the `testFromRomanCase` method (“`fromRoman` should only accept uppercase input”) was an error, because the call to `numeral.upper()` raised an `AttributeError` exception, because `toRoman` was supposed to return a string but didn't. But `testZero` (“`toRoman` should fail with 0 input”) was a failure, because the call to `fromRoman` did not raise the `InvalidRomanNumeral` exception that `assertRaises` was looking for.