深入Python3(八) 单元测试

0.摘要

  在此章节中,你将要编写及调试一系列用于阿拉伯数字与罗马数字相互转换的方法。并利用 p y t h o n python python u n i t t e s t unittest unittest模块进行单元测试。
  罗马数字的规则引出很多有意思的结果:

  1. 只有一种正确的途径用阿拉伯数字表示罗马数字。
  2. 反过来一样,一个字符串类型的有效的罗马数字也仅可以表示一个阿拉伯数字(即,这种转换方式也是只有一种)。
  3. 只有有限范围的阿拉伯数字可以以罗马数字表示,那就是1-3999。而罗马数字表示大数字却有几种方式。例如,为了表示一个数字连续出现时正确的值则需要乘以1000。为了达到本节的目的,限定罗马数字在 1 到 3999 之间。
  4. 无法用罗马数字来表示 0 。
  5. 无法用罗马数字来表示负数 。
  6. 无法用罗马数字来表示分数或非整数 。

  现在,开始设计 r o m a n . p y roman.py roman.py 模块。它有两个主要的方法: t o _ r o m a n ( ) to\_roman() to_roman() f r o m _ r o m a n ( ) from\_roman() from_roman() t o _ r o m a n ( ) to\_roman() to_roman() 方法接收一个从 1 到 3999 之间的整型数字,然后返回一个字符串类型的罗马数字。
  在这里停下来。现在让我们进行一些意想不到的操作:编写一个测试用例来检测 t o _ r o m a n to\_roman to_roman 函数是否实现了你想要的功能。你想得没错:你正在编写测试尚未编写代码的代码。
  这就是所谓的测试驱动开发或tdd。那两个转换方法( t o _ r o m a n ( ) to\_roman() to_roman() 及之后的 f r o m _ r o m a n ( ) from\_roman() from_roman())可以独立于任何使用它们的大程序而作为一个单元来被编写及测试。 P y t h o n Python Python 自带一个单元测试框架,被恰当地命名为 u n i t t e s t unittest unittest 模块。
  单元测试是整个以测试为中心的开发策略中的一个重要部分。编写单元测试应该安排在项目的早期,同时要让它随同代码及需求变更一起更新。很多人都坚持测试代码应该先于被测试代码的,而这种风格也是我在本节中所主张的。但是,不管你何时编写,单元测试都是有好处的。

  • 在编写代码之前,通过编写单元测试来强迫你使用有用的方式细化你的需求。
  • 在编写代码时,单元测试可以使你避免过度编码。当所有测试用例通过时,实现的方法就完成了。
  • 重构代码时,单元测试用例有助于证明新版本的代码跟老版本功能是一致的。
  • 在维护代码期间,如果有人对你大喊:你最新的代码修改破坏了原有代码的状态,那么此时单元测试可以帮助你反驳(“先生,我提交的代码通过了所有单元测试用例…”)。
  • 在团队编码中,缜密的测试套件可以降低你的代码影响别人代码的机会,这是因为你需要优先执行别人的单元测试用例。(我曾经在代码冲刺见过这种实践。一个团队把任务分解,每个人领取其中一小部分任务,同时为其编写单元测试;然后,团队相互分享他们的单元测试用例。这样,所有人都可以在编码过程中提前发现谁的代码与其他人的不可以良好工作。)

1.一个简单的问题

  一个测试用例仅回答一个关于它正在测试的代码问题。一个测试用例应该可以:

  • ……完全自动运行,而不需要人工干预。单元测试几乎是全自动的。
  • ……自主判断被测试的方法是通过还是失败,而不需要人工解释结果。
  • ……独立运行,而不依赖其它测试用例(即使测试的是同样的方法)。即,每一个测试用例都是一个孤岛。

  让我们据此为第一个需求建立一个测试用例: t o _ r o m a n ( ) to\_roman() to_roman() 方法应该返回代表1-3999的罗马数字。

import roman1
import unittest

class KnownValues(unittest.TestCase):               
    known_values = ( (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'))           

    def test_to_roman_known_values(self):           
        '''to_roman should give known result with known input'''
        for integer, numeral in self.known_values:
            result = roman1.to_roman(integer)       
            self.assertEqual(numeral, result)       

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

  这些代码功效如何并不那么显而易见。它定义了一个没有 _ _ i n i t _ _ \_\_init\_\_ __init__ 方法的类。而该类当然有其它方法,但是这些方法都不会被调用。在整个脚本中,有一个 _ _ m a i n _ _ \_\_main\_\_ __main__ 块,但它并不引用该类及它的方法。但我承诺,它做别的事情了。
  为了编写测试用例,首先使该测试用例类成为 u n i t t e s t unittest unittest 模块的 T e s t C a s e TestCase TestCase 类的子类。 T e s t C a s e TestCase TestCase 提供了很多你可以用于测试特定条件的测试用例的有用的方法。
  这是一张我手工核实过的整型数字-罗马数字对的列表。它包括最小的十个数字、最大数字、每一个有唯一一个字符串格式的罗马数字的数字以及一个有其它有效数字产生的随机数。你没有必要测试每一个可能的输入,而需要测试所有明显的边界用例。
  每一个独立的测试都有它自己的不含参数及没有返回值的方法。如果方法不抛出异常而正常退出则认为测试通过;否则,测试失败。
  在方法中调用了真实的 t o _ r o m a n ( ) to\_roman() to_roman() 方法(当然,该方法还没编写)。注意,现在你已经为 t o _ r o m a n ( ) to\_roman() to_roman() 方法定义了接口:它的参数是一个整型数字,它会返回一个字符串(罗马数字的表示形式)。如果接口实现与这些定义不一致,那么测试就会被视为失败。同样,当你调用 t o _ r o m a n ( ) to\_roman() to_roman() 时,不要捕获任何异常。这些都是 u n i t t e s t unittest unittest 特意设计的。当你以有效的输入调用 t o _ r o m a n ( ) to\_roman() to_roman() 时它不会抛出异常。如果 t o _ r o m a n ( ) to\_roman() to_roman() 抛出了异常,则测试被视为失败。
  假设 t o _ r o m a n ( ) to\_roman() to_roman() 方法已经被正确定义,正确调用,成功实现以及返回了一个值,那么最后一步就是去检查它的返回值是否正确 。这是测试中一个普遍的问题。 T e s t C a s e TestCase TestCase 类提供了一个方法 a s s e r t E q u a l assertEqual assertEqual 来检查两个值是否相等。如果 t o _ r o m a n ( ) to\_roman() to_roman() 的返回值跟已知的期望值不一致,则抛出异常,并且测试失败。如果两值相等, a s s e r t E q u a l assertEqual assertEqual 不会做任何事情。如果 t o _ r o m a n ( ) to\_roman() to_roman() 的所有返回值均与已知的期望值一致,则 a s s e r t E q u a l assertEqual assertEqual 不会抛出任何异常,于是, t e s t _ t o _ r o m a n _ k n o w n _ v a l u e s test\_to\_roman\_known\_values test_to_roman_known_values 最终会会正常退出,这就意味着 t o _ r o m a n ( ) to\_roman() to_roman() 通过此次测试。
  一旦你有了测试用例,你就可以开始编写 t o _ r o m a n ( ) to\_roman() to_roman() 方法。首先,你应该用一个空方法作为存根,同时确认该测试失败。因为如果在编写任何代码之前测试已经通过,那么你的测试对你的代码是完全不会有效果的!单元测试就像跳舞:测试先行,编码跟随。编写一个失败的测试,然后进行编码直到该测试通过。
在这里插入图片描述
在这里插入图片描述

  运行脚本就会执行 u n i t t e s t . m a i n ( ) unittest.main() unittest.main() , 该方法执行了每一条测试用例。而每一条测试用例都是 r o m a n t e s t . p y romantest.py romantest.py 中的类方法。这些测试类没有必要的组织要求;它们每一个都包括一个独立的测试方法,或者你也可以编写一个含有多个测试方法的类。唯一的要求就是每一个测试类都必须继承 u n i t t e s t . T e s t C a s e unittest.TestCase unittest.TestCase
  对于每一个测试用例, u n i t t e s t unittest unittest 模块会打印出测试方法的 d o c s t r i n g docstring docstring ,并且说明该测试是失败还是成功。正如预期那样,该测试用例失败了。
  对于每一个失败的测试用例, u n i t t e s t unittest unittest 模块会打印出详细的跟踪信息。在该用例中, a s s e r t E q u a l ( ) assertEqual() assertEqual() 的调用抛出了一个$ AssertionError$ 的异常,这是因为 t o _ r o m a n ( 1 ) to\_roman(1) to_roman(1) 本应该返回 ‘I’ 的,但是它没有。
  在说明每个用例的详细执行结果之后, u n i t t e s t unittest unittest 打印出一个简述来说明“多少用例被执行了”和“测试执行了多长时间”。
   u n i t t e s t unittest unittest 可以区别用例执行失败和程序错误,后者是一种异常,它是由被测试的代码或者单元测试用例本身的代码问题而引起的。
  现在,可以实现 t o _ r o m a n ( ) to\_roman() to_roman() 方法了。
在这里插入图片描述

  检查它能否通过测试用例:
在这里插入图片描述

  通过了,这说明你的程序基本上(因为测试用例并没有覆盖完全)可以正确处理正常输入了,但是如果是异常输入呢?

2.处理非法输入

  仅仅在“正常”值时证明方法通过的测试是不够的;你同样需要测试当输入“非法”值时方法失败。但并不是说要枚举所有的失败类型,而是说必要在你预期的范围内失败。
在这里插入图片描述

  这明显不是你所期望的──那也不是一个合法的罗马数字!事实上,这些输入值都超过了允许的范围,但该函数却返回了假值。悄悄返回的错误值是 很糟糕 的,因为如果一个程序要挂掉的话,迅速且引人注目地挂掉会好很多。
在这里插入图片描述

  顺便提一下,这里的代码添加到之前的文件就行了。
  如前一个测试用例,创建一个继承于 u n i t t e s t . T e s t C a s e unittest.TestCase unittest.TestCase 的类。你可以在每个类中实现多个测试(正如你在本节中将会看到的一样),但是我却选择了创建一个新类,因为该测试与上一个有点不同。这样,我们可以把正常输入的测试跟非法输入的测试分别放入不同的两个类中。
   u n i t t e s t . T e s t C a s e unittest.TestCase unittest.TestCase 类提供 a s s e r t R a i s e s assertRaises assertRaises 方法,该方法需要以下参数:你期望的异常、你要测试的方法及传入给方法的参数(如果被测试的方法需要多个参数的话,则把所有参数依次传入 a s s e r t R a i s e s assertRaises assertRaises, 它会正确地把参数传递给被测方法的)。
  请关注代码的最后一行。这里并不需要直接调用 t o _ r o m a n ( ) to\_roman() to_roman() ,同时也不需要手动检查它抛出的异常类型,这些 a s s e r t R a i s e s assertRaises assertRaises 方法都给我们完成了。你要做的所有事情就是告诉 a s s e r t R a i s e s assertRaises assertRaises你期望的异常类型( r o m a n 2. O u t O f R a n g e E r r o r roman2.OutOfRangeError roman2.OutOfRangeError)、被测方法( t o _ r o m a n ( ) to\_roman() to_roman())以及方法的参数(4000)。assertRaises 方法负责调用 t o _ r o m a n ( ) to\_roman() to_roman() 和检查方法抛出 r o m a n 2. O u t O f R a n g e E r r o r roman2.OutOfRangeError roman2.OutOfRangeError 的异常。
在这里插入图片描述

  测试本应该是失败的(因为并没有任何代码使它通过),但是它没有真正的“失败”,而是出现了“错误”。这里有些微妙但是重要的区别。单元测试事实上有三种返回值:通过、失败以及错误。“通过”,但当然就是说测试成功了──被测代码符合你的预期。“失败”就是就如之前的测试用例一样(直到你编写代码令它通过)──执行了被测试的代码但返回值并不是所期望的。“错误”就是被测试的代码甚至没有正确执行。
  为什么代码没有正确执行呢?回溯说明了一切。你正在测试的模块没有叫 O u t O f R a n g e E r r o r OutOfRangeError OutOfRangeError 的异常。回忆一下,该异常是你传递给 a s s e r t R a i s e s ( ) assertRaises() assertRaises() 方法的,因为你期望当传递给被测试方法一个超大值时可以抛出该异常。但是,该异常并不存在,因此 a s s e r t R a i s e s ( ) assertRaises() assertRaises() 的调用会失败。事实上测试代码并没有机会测试 t o _ r o m a n ( ) to\_roman() to_roman() 方法,因为它还没有到达那一步。
在这里插入图片描述

  异常也是类。“越界”错误是值错误的一类──参数值超出了可接受的范围。所以,该异常继承了内建的 V a l u e E r r o r ValueError ValueError 异常类。这并不是严格的要求(它同样也可以继承于基类 E x c e p t i o n Exception Exception),只要它正确就行了。
  事实上,异常类可以不做任何事情,但是至少添加一行代码使其成为一个类。 p a s s pass pass 的真正意思是什么都不做,但是它是一行 P y t h o n Python Python代码,所以可以使其成为类。
在这里插入图片描述

  新的测试仍然没有通过,但是它并没有返回错误而是失败。这就是进步!它意味着这回 a s s e r t R a i s e s ( ) assertRaises() assertRaises() 方法的调用是成功的,同时,单元测试框架事实上也测试了 t o _ r o m a n ( ) to\_roman() to_roman() 函数。
  当然 t o _ r o m a n ( ) to\_roman() to_roman() 方法没有引发你所定义的 O u t O f R a n g e E r r o r OutOfRangeError OutOfRangeError 异常,因为你并没有让它这么做。这真是个好消息!因为它意味着这是个合格的测试案例——在编写代码使之通过之前它将会以失败为结果。
在这里插入图片描述
在这里插入图片描述

   万岁!两个测试都通过了。因为你是在测试与编码之间来回反复开发的,所以你可以肯定使得其中一个测试从“失败”转变为“通过”的原因就是你刚才新添的两行代码。虽然这种信心来得并不简单,但是这种代价会在你代码的生命周期中得到回报。

3.More Halting, More Fire

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.还有一件事情……

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.可喜的对称性

  多亏我们定义的用于单个罗马数字映射至阿拉伯数字的良好的数据结构, f r o m _ r o m a n ( ) from\_roman() from_roman() 的实现本质上与 t o _ r o m a n ( ) to\_roman() to_roman() 一样简单。
  不过,测试先行!为了证明其准确性,我们将需要一个对“已知取值”进行的测试。我们的测试套件已经包含了一个已知取值的映射表,那么,我们就重用它。
在这里插入图片描述

  这里看到了令人高兴的对称性。 t o _ r o m a n ( ) to\_roman() to_roman() f r o m _ r o m a n ( ) from\_roman() from_roman() 函数是互逆的。前者把整型数字转换为特殊格式化的字符串,而后者则把特殊格式化的字符串转换为整型数字。理论上,我们应该可以使一个数字“绕一圈”,即把数字传递给 t o _ r o m a n ( ) to\_roman() to_roman() 方法,得到一个字符串;然后把该字符串传入 f r o m _ r o m a n ( ) from\_roman() from_roman() 方法,得到一个整型数字,并且跟传给 t o _ r o m a n ( ) to\_roman() to_roman()方法的数字是一样的。据此我们可以设计出一个测试用例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  嘿,你注意到了么?我定义了一个除了 d o c s t r i n g docstring docstring 之外没有任何东西的方法。这是合法的 P y t h o n Python Python 代码。事实上,一些程序员喜欢这样做。“不要留空;写点文档!”

在这里插入图片描述
在这里插入图片描述

  此处的匹配模式与 t o _ r o m a n ( ) to\_roman() to_roman() 完全相同。遍历整个罗马数字数据结构 (一个元组的元组),与前面不同的是不去一个个地搜索最大的整数,而是搜寻 “最大的”罗马数字字符串。
在这里插入图片描述

  这儿有两个令人激动的消息。一个是 f r o m _ r o m a n ( ) from\_roman() from_roman() 对于所有有效输入运转正常,至少对于你测试的已知值是这样。第二个好消息是,完备性测试也通过了。与已知值测试的通过一起来看,你有理由相信 t o _ r o m a n ( ) to\_roman() to_roman() f r o m _ r o m a n ( ) from\_roman() from_roman() 对于所有有效输入值工作正常。(尚不能完全相信,理论上存在这种可能性: t o _ r o m a n ( ) to\_roman() to_roman() 存在错误而导致一些特定输入会产生错误的罗马数字表示,并且 f r o m _ r o m a n ( ) from\_roman() from_roman() 也存在相应的错误,把 t o _ r o m a n ( ) to\_roman() to_roman() 错误产生的这些罗马数字错误地转换为最初的整数。取决于你的应用程序和你的要求,你或许需要考虑这个可能性;如果是这样,编写更全面的测试用例直到解决这个问题。

6.更多错误输入

  现在 f r o m _ r o m a n ( ) from\_roman() from_roman() 对于有效输入能够正常工作了,是揭开最后一个谜底的时候了:使它正常工作于无效输入的情况下。这意味着要找出一个方法检查一个字符串是不是有效的罗马数字。这比中验证有效的数字输入困难,但是你可以使用一个强大的工具:正则表达式(如果你不熟悉正则表达式,现在是该好好读读正则表达式那一章节的时候了)。
  如你在个案研究:罗马字母s中所见到的,构建罗马数字有几个简单的规则:使用的字母M , D , C , L , X , V和I 。让我们回顾一下:
在这里插入图片描述

   1. 1. 1.大部分时候用字符相叠加来表示数字。 I I I是1, I I II II是2, I I I III III是3。 V I VI VI是6(挨个看来,是“5 和 1”的组合), V I I VII VII是7, V I I I VIII VIII是8。
   2. 2. 2.含有10的字符( I I I X X X C C C M M M)最多可以重复出现三个。为了表示4,必须用同一位数的下一个更大的数字5来减去一。不能用 I I I I IIII IIII来表示4,而应该是 I V IV IV(意思是比5小1)。40写做 X L XL XL(比50小10),41写做 X L I XLI XLI,42写做 X L I I XLII XLII,43写做 X L I I I XLIII XLIII,44写做 X L I V XLIV XLIV(比50小10并且比5小1)。
   3. 3. 3.为了表示一个中间的数字,需要从一个最终的值来减。比如:9需要从10来减:8是 V I I I VIII VIII,但9是 I X IX IX(比10小1),并不是 V I I I I VIIII VIIII I I I字符不能重复4次)。90是 X C XC XC,900是 C M CM CM
   4. 4. 4.表示5的字符不能在一个数字中重复出现。10只能用 X X X表示,不能用 V V VV VV表示。100只能用 C C C表示,而不是 L L LL LL
   5. 5. 5.罗马数字是从左到右来计算,因此字符的顺序非常重要。 D C DC DC表示600,而 C D CD CD完全是另一个数字400(比500小100)。 C I CI CI是101, I C IC IC不是一个罗马数字(因为你不能从100减1,你只能写成 X C I X XCIX XCIX,表示比100小10,且比10小1)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值