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

Dive Into Python-Chapter 15. Refactoring

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 (174.14 KB, 49 trang )


Chapter 15. Refactoring
15.1. Handling bugs

Despite your best efforts to write comprehensive unit tests, bugs happen.
What do I mean by “bug”? A bug is a test case you haven't written yet.
Example 15.1. The bug

>>> import roman5
>>> roman5.fromRoman("") 1
0

1 Remember in the previous section when you kept seeing that an
empty string would match the regular expression you were using to check
for valid Roman numerals? Well, it turns out that this is still true for the final
version of the regular expression. And that's a bug; you want an empty string
to raise an InvalidRomanNumeralError exception just like any other
sequence of characters that don't represent a valid Roman numeral.

After reproducing the bug, and before fixing it, you should write a test case
that fails, thus illustrating the bug.
Example 15.2. Testing for the bug (romantest61.py)

class FromRomanBadInput(unittest.TestCase):

# previous test cases omitted for clarity (they haven't changed)

def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, "") 1



1 Pretty simple stuff here. Call fromRoman with an empty string and
make sure it raises an InvalidRomanNumeralError exception. The hard part
was finding the bug; now that you know about it, testing for it is the easy
part.

Since your code has a bug, and you now have a test case that tests this bug,
the test case will fail:
Example 15.3. Output of romantest61.py against roman61.py

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

======================================================
================
FAIL: fromRoman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in

testBlank
self.assertRaises(roman61.InvalidRomanNumeralError,
roman61.fromRoman, "")
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 13 tests in 2.864s

FAILED (failures=1)

Now you can fix the bug.
Example 15.4. Fixing the bug (roman62.py)

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

def fromRoman(s):
"""convert Roman numeral to integer"""
if not s: 1
raise InvalidRomanNumeralError, 'Input can not be blank'
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s

result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result


1 Only two lines of code are required: an explicit check for an empty
string, and a raise statement.
Example 15.5. Output of romantest62.py against roman62.py

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

----------------------------------------------------------------------
Ran 13 tests in 2.834s

OK 2

1 The blank string test case now passes, so the bug is fixed.
2 All the other test cases still pass, which means that this bug fix didn't
break anything else. Stop coding.

Coding this way does not make fixing bugs any easier. Simple bugs (like
this one) require simple test cases; complex bugs will require complex test

cases. In a testing-centric environment, it may seem like it takes longer to fix
a bug, since you need to articulate in code exactly what the bug is (to write
the test case), then fix the bug itself. Then if the test case doesn't pass right
away, you need to figure out whether the fix was wrong, or whether the test
case itself has a bug in it. However, in the long run, this back-and-forth
between test code and code tested pays for itself, because it makes it more
likely that bugs are fixed correctly the first time. Also, since you can easily
re-run all the test cases along with your new one, you are much less likely to
break old code when fixing new code. Today's unit test is tomorrow's
regression test.
15.2. Handling changing requirements

Despite your best efforts to pin your customers to the ground and extract
exact requirements from them on pain of horrible nasty things involving
scissors and hot wax, requirements will change. Most customers don't know
what they want until they see it, and even if they do, they aren't that good at
articulating what they want precisely enough to be useful. And even if they
do, they'll want more in the next release anyway. So be prepared to update
your test cases as requirements change.

Suppose, for instance, that you wanted to expand the range of the Roman
numeral conversion functions. Remember the rule that said that no character
could be repeated more than three times? Well, the Romans were willing to
make an exception to that rule by having 4 M characters in a row to
represent 4000. If you make this change, you'll be able to expand the range
of convertible numbers from 1..3999 to 1..4999. But first, you need to make
some changes to the test cases.
Example 15.6. Modifying test cases for new requirements (romantest71.py)

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


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

import roman71
import unittest

class KnownValues(unittest.TestCase):
knownValues = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),

(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),

(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'),
(4000, 'MMMM'), 1
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX'))

def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.toRoman(integer)
self.assertEqual(numeral, result)

def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)

class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000)
2

def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0)


def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1)

def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5)

class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): 3
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)

def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)

def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)

def testBlank(self):

"""fromRoman should fail with blank string"""
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, "")

class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 5000): 4
numeral = roman71.toRoman(integer)
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)

class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
self.assertEqual(numeral, numeral.upper())

def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
roman71.fromRoman(numeral.upper())
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, numeral.lower())

if __name__ == "__main__":
unittest.main()


1 The existing known values don't change (they're all still reasonable
values to test), but you need to add a few more in the 4000 range. Here I've
included 4000 (the shortest), 4500 (the second shortest), 4888 (the longest),
and 4999 (the largest).
2 The definition of “large input” has changed. This test used to call
toRoman with 4000 and expect an error; now that 4000-4999 are good
values, you need to bump this up to 5000.
3 The definition of “too many repeated numerals” has also changed.
This test used to call fromRoman with 'MMMM' and expect an error; now
that MMMM is considered a valid Roman numeral, you need to bump this
up to 'MMMMM'.
4 The sanity check and case checks loop through every number in the
range, from 1 to 3999. Since the range has now expanded, these for loops
need to be updated as well to go up to 4999.

Now your test cases are up to date with the new requirements, but your code
is not, so you expect several of the test cases to fail.
Example 15.7. Output of romantest71.py against roman71.py


fromRoman should only accept uppercase input ... ERROR 1
toRoman should always return uppercase ... ERROR
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ERROR 2
toRoman should give known result with known input ... ERROR 3
fromRoman(toRoman(n))==n for all n ... ERROR 4
toRoman should fail with non-integer input ... ok

toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

1 Our case checks now fail because they loop from 1 to 4999, but
toRoman only accepts numbers from 1 to 3999, so it will fail as soon the test
case hits 4000.
2 The fromRoman known values test will fail as soon as it hits
'MMMM', because fromRoman still thinks this is an invalid Roman
numeral.

×