<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            15.2. 應對需求變化

            盡管你竭盡努力地分析你的客戶,并點燈熬油地提煉出精確的需求,但需求還是會是不斷變化。大部分客戶在看到產品前不知道他們想要什么。即便知道,也不擅于精確表述出他們的有效需求。即便能表述出來,他們在下一個版本一定會要求更多的功能。因此你需要做好更新測試用例的準備以應對需求的改變。

            假設你想要擴展羅馬數字轉換函數的范圍。還記得沒有哪個字符可以重復三遍以上這條規則嗎?呃,現在羅馬人希望給這條規則來個例外,用連續出現 4 個 M 字符來表示 4000。如果這樣改了,你就可以把轉換范圍從 1..3999 擴展到 1..4999。但你先要對測試用例進行修改。

            例 15.6. 修改測試用例以適應新需求 (romantest71.py)

            這個文件可以在例子目錄下的 py/roman/stage7/ 目錄中找到。

            如果您還沒有下載本書附帶的樣例程序, 可以 下載本程序和其他樣例程序

            
            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 原來的已知值沒有改變 (它們仍然是合理的測試值) 但你需要添加幾個大于 4000 的值。這里我添加了 4000 (最短的一個),4500 (次短的一個),4888 (最長的一個) 和 4999 (值最大的一個)。
            2 最大輸入”的定義改變了。以前是以 4000 調用 toRoman 并期待一個錯誤;而現在 4000-4999 成為了有效輸入,需要將這個最大輸入提升至 5000
            3 過多字符重復” 的定義也改變了。這個測試以前是以 'MMMM' 調用 fromRoman 并期待一個錯誤;而現在 MMMM 被認為是一個有效的羅馬數字表示,需要將這個“過多字符重復”改為 'MMMMM'
            4 完備測試和大小寫測試原來在 13999 范圍內循環。現在范圍擴展了,這個 for 循環需要將范圍也提升至 4999

            現在你的測試用例和新需求保持一致了,但是你的程序代碼還沒有,因此幾個測試用例的失敗是意料之中的事。

            例 15.7. 用 romantest71.py 測試 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 我們的大小寫檢查是因為循環范圍是 14999,而 toRoman 只接受 13999 之間的數,因此測試循環到 4000 就會失敗。
            2 fromRoman 的已知值測試在遇到 'MMMM' 就會失敗,因為 fromRoman 還認為這是一個無效的羅馬數字表示。
            3 toRoman 的已知值測試在遇到 4000 就會失敗,因為 toRoman 仍舊認為這超出了有效值范圍。
            4 完備測試在遇到 4000 也會失敗,因為 toRoman 也會認為這超出了有效值范圍。
            
            ======================================================================
            ERROR: fromRoman should only accept uppercase input
            ----------------------------------------------------------------------
            Traceback (most recent call last):
              File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in testFromRomanCase
                numeral = roman71.toRoman(integer)
              File "roman71.py", line 28, in toRoman
                raise OutOfRangeError, "number out of range (must be 1..3999)"
            OutOfRangeError: number out of range (must be 1..3999)
            ======================================================================
            ERROR: toRoman should always return uppercase
            ----------------------------------------------------------------------
            Traceback (most recent call last):
              File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in testToRomanCase
                numeral = roman71.toRoman(integer)
              File "roman71.py", line 28, in toRoman
                raise OutOfRangeError, "number out of range (must be 1..3999)"
            OutOfRangeError: number out of range (must be 1..3999)
            ======================================================================
            ERROR: fromRoman should give known result with known input
            ----------------------------------------------------------------------
            Traceback (most recent call last):
              File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in testFromRomanKnownValues
                result = roman71.fromRoman(numeral)
              File "roman71.py", line 47, in fromRoman
                raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
            InvalidRomanNumeralError: Invalid Roman numeral: MMMM
            ======================================================================
            ERROR: toRoman should give known result with known input
            ----------------------------------------------------------------------
            Traceback (most recent call last):
              File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in testToRomanKnownValues
                result = roman71.toRoman(integer)
              File "roman71.py", line 28, in toRoman
                raise OutOfRangeError, "number out of range (must be 1..3999)"
            OutOfRangeError: number out of range (must be 1..3999)
            ======================================================================
            ERROR: fromRoman(toRoman(n))==n for all n
            ----------------------------------------------------------------------
            Traceback (most recent call last):
              File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in testSanity
                numeral = roman71.toRoman(integer)
              File "roman71.py", line 28, in toRoman
                raise OutOfRangeError, "number out of range (must be 1..3999)"
            OutOfRangeError: number out of range (must be 1..3999)
            ----------------------------------------------------------------------
            Ran 13 tests in 2.213s
            
            FAILED (errors=5)

            既然新的需求導致了測試用例的失敗,你該考慮修改代碼以便它能再次通過測試用例。(在你開始編寫單元測試時要習慣一件事:被測試代碼永遠不會在編寫測試用例“之前”編寫。正因為如此,你還有一些工作要做,一旦可以通過所有的測試用例,停止編碼。)

            例 15.8. 為新的需求編寫代碼 (roman72.py)

            這個文件可以在例子目錄下的 py/roman/stage7/ 目錄中找到。

            """Convert to and from Roman numerals"""
            import re
            
            #Define exceptions
            class RomanError(Exception): pass
            class OutOfRangeError(RomanError): pass
            class NotIntegerError(RomanError): pass
            class InvalidRomanNumeralError(RomanError): pass
            
            #Define digit mapping
            romanNumeralMap = (('M',  1000),
                               ('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"""
                if not (0 < n < 5000):                                                         1
                    raise OutOfRangeError, "number out of range (must be 1..4999)"
                if int(n) <> n:
                    raise NotIntegerError, "non-integers can not be converted"
            
                result = ""
                for numeral, integer in romanNumeralMap:
                    while n >= integer:
                        result += numeral
                        n -= integer
                return result
            
            #Define pattern to detect valid Roman numerals
            romanNumeralPattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$' 2
            
            def fromRoman(s):
                """convert Roman numeral to integer"""
                if not s:
                    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 toRoman 只需要在取值范圍檢查一處做個小改動。將原來的 0 < n < 4000,更改為現在的檢查 0 < n < 5000。你還要更改你 raise 的錯誤信息以反映接受新取值范圍 (1..4999 而不再是 1..3999)。你不需要改變函數的其他部分,它們已經適用于新的情況。(它們會欣然地為新的 1000 添加 'M',以 4000 為例,函數會返回 'MMMM' 。之前沒能這樣做是因為到范圍檢查時就被停了下來。)
            2 你對 fromRoman 也不需要做過多的修改。唯一的修改就在 romanNumeralPattern:如果你注意的話,你會發現你只需在正則表達式的第一部分增加一個可選的 M 。這就允許最多 4 個 M 字符而不再是 3 個,意味著你允許代表 4999 而不只是 3999 的羅馬數字。fromRoman 函數本身是普遍適用的,它并不在意字符被多少次的重復,只是根據重復的羅馬字符對應的數值進行累加。以前沒能處理 'MMMM' 是因為你通過正則表達式的檢查強行停止了。

            你可能會懷疑只需這兩處小改動。嘿,不相信我的話,你自己看看吧:

            例 15.9. 用 romantest72.py 測試 roman72.py 的結果

            fromRoman should only accept uppercase input ... ok
            toRoman should always return uppercase ... ok
            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 ... 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 3.685s
            
            OK 1
            1 所有的測試用例都通過了,停止編寫代碼。

            全面的單元測試意味著不必依賴于程序員的一面之詞:“相信我!

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线