Tải bản đầy đủ (.pdf) (53 trang)

Dive Into Python-Chapter 14. Test-First Programming

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (180.21 KB, 53 trang )


Chapter 14. Test-First Programming
14.1. roman.py, stage 1

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

This file is available in py/roman/stage1/ in the examples directory.

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 1
class OutOfRangeError(RomanError): pass 2
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass 3

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

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


1 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.
2 The OutOfRangeError and NotIntegerError exceptions will eventually
be used by toRoman to flag various forms of invalid input, as specified in
ToRomanBadInput.
3 The InvalidRomanNumeralError exception will eventually be used by
fromRoman to flag invalid input, as specified in FromRomanBadInput.
4 At this stage, you want to define the API of each of your functions,
but you don't want to code them yet, so you stub them out using the Python
reserved word pass.

Now for the big moment (drum roll please): you're finally going to run the
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, you should go back to
romantest.py and re-evaluate why you 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 you can see exactly what's going on as each test case runs.
With any luck, your output should look like this:
Example 14.2. 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
testNonInteger
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 1
----------------------------------------------------------------------
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 2
----------------------------------------------------------------------
Ran 12 tests in 0.040s 3

FAILED (failures=10, errors=2) 4

1 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 the test cases passed.

2 For each failed test case, unittest displays the trace information
showing exactly what happened. In this case, the call to assertRaises (also
called failUnlessRaises) raised an AssertionError because it was expecting
toRoman to raise an OutOfRangeError and it didn't.
3 After the detail, unittest displays a summary of how many tests were
performed and how long it took.
4 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 you'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.
14.2. roman.py, stage 2

Now that you have the framework of the roman module laid out, it's time to
start writing code and passing test cases.
Example 14.3. roman2.py

This file is available in py/roman/stage2/ in the examples directory.

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

#Define digit mapping
romanNumeralMap = (('M', 1000), 1
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))

def toRoman(n):
"""convert integer to Roman numeral"""
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer: 2
result += numeral
n -= integer
return result


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

1 romanNumeralMap is a tuple of tuples which defines three things:

1. The character representations of the most basic Roman numerals. Note
that this is not just the single-character Roman numerals; you're also
defining two-character pairs like CM (“one hundred less than one
thousand”); this will make the toRoman code simpler later.
2. The order of the Roman numerals. They are listed in descending value
order, from M all the way down to I.
3. The value of each Roman numeral. Each inner tuple is a pair of
(numeral, value).

2 Here's where your rich data structure pays off, because you don't need
any special logic to handle the subtraction rule. To convert to Roman
numerals, you simply iterate through romanNumeralMap looking for the
largest integer value less than or equal to the input. Once found, you add the
Roman numeral representation to the end of the output, subtract the
corresponding integer value from the input, lather, rinse, repeat.
Example 14.4. How toRoman works

If you're not clear how toRoman works, add a print statement to the end of
the while loop:

while n >= integer:
result += numeral
n -= integer

print 'subtracting', integer, 'from input, adding', numeral, 'to output'

>>> import roman2
>>> roman2.toRoman(1424)
subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
'MCDXXIV'

So toRoman appears to work, at least in this manual spot check. But will it
pass the unit testing? Well no, not entirely.
Example 14.5. Output of romantest2.py against roman2.py

Remember to run romantest2.py with the -v command-line flag to enable
verbose mode.

fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok 1
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 ... ok 2
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL 3
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL


1 toRoman does, in fact, always return uppercase, because
romanNumeralMap defines the Roman numeral representations as
uppercase. So this test passes already.
2 Here's the big news: this version of the toRoman function passes the
known values test. Remember, it's not comprehensive, but it does put the
function through its paces with a variety of good inputs, including inputs that
produce every single-character Roman numeral, the largest possible input
(3999), and the input that produces the longest possible Roman numeral
(3888). At this point, you can be reasonably confident that the function
works for any good input value you could throw at it.
3 However, the function does not “work” for bad values; it fails every
single bad input test. That makes sense, because you didn't include any
checks for bad input. Those test cases look for specific exceptions to be
raised (via assertRaises), and you're never raising them. You'll do that in the
next stage.

Here's the rest of the output of the unit test, listing the details of all the
failures. You're down to 10.


======================================================
================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in
testFromRomanCase
roman2.fromRoman, numeral.lower())
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises

raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================
================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in
testMalformedAntecedent
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.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\stage2\romantest2.py", line 127, in
testRepeatedPairs
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.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\stage2\romantest2.py", line 122, in
testTooManyRepeatedNumerals
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.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\stage2\romantest2.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: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.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\stage2\romantest2.py", line 116, in
testNonInteger
self.assertRaises(roman2.NotIntegerError, roman2.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\stage2\romantest2.py", line 112, in
testNegative
self.assertRaises(roman2.OutOfRangeError, roman2.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\stage2\romantest2.py", line 104, in
testTooLarge
self.assertRaises(roman2.OutOfRangeError, roman2.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\stage2\romantest2.py", line 108, in testZero
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0)

×