15.2 使用doctest来定义测试用例

    相比unittest模块,doctest模块为我们提供了一种相对简单的测试方式。对于很多简单交互的用例,可以在docstring中表示,并使用doctest来进行自动化测试。它会将文档和测试用例合并为一个包。

    对于模块、类、方法或函数,doctest的用例被写成了docstring。在一个doctest用例中,向我们展示了Python中交互的提示>>>、语句和回复。在doctest模块中,包含了一个在docstring中查找这些示例的应用。它运行指定的示例,并将docstring中期望的结果与实际结果进行对比。

    对于更大的、更复杂的类定义而言,可能会有一些挑战。在一些用例中,可以看到这样的做法使得例子中打印出的结果很难被使用,需要使用unittest中更复杂类或函数来进行比较。

    如果设计API时足够谨慎,就可以创建一个可以用于交互的类。如果它可被用于交互,然后就可以基于交互来创建一个doctest的例子。

    事实上,在一个设计良好的类中有两个属性可以被用于交互,并且在文档字符串中包括了doctest的例子。许多内置的模块包含了在API中doctest的例子。我们所下载的许多其他包中也会包含doctest的例子。

    对于简单的函数,可以提供如下所示的文档。

    def ackermann( m, n ):
       """Ackermann's Function
       ackermann( m, n ) -> 2↑^{m-2}(n+3) - 3

       See http://en.wikipedia.org/wiki/Ackermann_function and
       http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation.

       >>> from p3_c15 import ackermann
       >>> ackermann(2,4)
       11
       >>> ackermann(0,4)
       5
       >>> ackermann(1,0)
       2
       >>> ackermann(1,1)
       3

       """
       if m == 0: return n+1
       elif m > 0 and n == 0: return ackermann( m-1, 1 )
       elif m > 0 and n > 0: return ackermann( m-1, ackermann( m, n-1 ) )

    我们定义了ackermann函数的一个版本,包括了一些docstring注释,其中有与Python交互的5个示例回复。第1个示例回复是import语句,不应该包含输出,在其余4个示例输出中展示了函数的不同返回值。

    在这个用例中,结果都是正确的。没有留下任何隐藏的bug给读者作为测试,我们可以使用doctest模块来运行这些测试。当作为程序来运行时,命令行参数就是需要被测试的文件。doctest程序对所有的docstring进行查找,并在这些字符串中查找要进行交互的Python例子。在doctest文档中提供了用于字符串查找的正则表达式,这一点很重要。在我们的例子中,为了辅助doctest解析器的工作,在最后一个doctest例子后面添加了一个很难发现的空白行。

    可以使用命令行来运行doctest

    python3.3 -m doctest p3_c15.py

    如果一切正常,不会有任何提示。可以通过添加-v选项来查看执行的详细结果。

    python3.3 -m doctest -v p3_c15.py

    它将显示出每个docstring以及每个从docstring中所返回的测试用例的详细信息。

    不需要任何引用任何测试以及包含测试的组件,它就能显示出不同的类、函数以及方法。这样就可以确定我们的测试在docstring中被正确地格式化了。

    在一些用例中会包含一些输出,在使用Python与这些输出交互起来很困难。在这些用例中,需要为docstring提供一些注释,说明一下如何对测试用例以及所期望的执行结果进行解析。

    对于复杂的输出,可以使用一种特殊的注释字符串。可以使用以下所示的两种方式中的任何一种来启用(或禁用)已有的指令,以下是第1种命令。

    # doctest: +DIRECTIVE

    以下是第2种命令。

    # doctest: -DIRECTIVE

    在期望结果的处理上,有多种修改方式。大多数是关于空格特殊处理的场景,以及如何对比实际结果与期望结果。

    有关完全匹配原则,在doctest文档中是这样强调的。

    “doctest是重要的,需要与期望结果完全匹配”。

    即使是一个字符不匹配,测试也会失败。需要向一些期望的输出中添加一些灵活性。如果发现添加灵活性很困难,unittest是一个不错的选择。

    以下是一些特殊情况,期望结果与实际结果匹配起来不是很容易。

    • Python不保证字典键的顺序。使用另一种结构来代替some dict,例如sorted (some dict.items())。
    • 方法函数id()和repr()涉及物理内存地址,如果使用#doctest: +ELLIPSIS指令来显示id()和repr()并在示例输出中使用…来替换ID和地址,Python不保证它们是一致的。
    • 浮点数值在不同平台可能是不同的。总是使用格式化或对数位进行截取的方式来显示浮点数是有意义的。可以使用"{:.4f}".format(value)或round(value,4)来确保非符号位被忽略了。
    • 在Python中,并不保证集合中元素的顺序。可以使用sorted(some set)来代替some set。
    • 不应当使用当前的日期或时间,因为它们不能保证一致性。涉及日期或时间的测试需要使用一个特殊的时间或日期,一般通过模拟的time或者datetime来实现。
    • 操作系统中的参数,例如文件大小或者时间戳,都是经常变化的,不应该使用。有时,可能会在doctest脚本中包含安装和卸载来管理OS资源。在其他情况下,推荐对os模块进行模仿。

    以上的考虑点意味着,我们的 doctest 模块中可能会包含一些额外的处理,而不仅仅是API,可能使用过类似以下这样的代码。

    >>> sum(values)/len(values)
    3.142857142857143

    它展示了从一种特殊实现中返回的完整输出。我们不能简单地将它复制粘贴到docstring中,浮点数结果可能会不同。需要做如下这样的修改。

    >>> round(sum(values)/len(values),4)
    3.1429

    这个值被四舍五入了,在不同的实现中是不会变化的。

    15.2.1 将doctest与unittest相结合

    doctest 模块中有一个钩子,可以基于 docstring 的注释创建一个适当的unittest.TestSuite。这意味着,可以在大应用中同时使用doctestunittest

    我们需要创建一个doctest.DocTestSuite()实例。它会从一个模块的docstring中创建一个组件。如果没有指定模块,将使用当前运行的模块创建组件,可以像如下代码这样来使用一个模块。

    import doctest
    suite5= doctest.DocTestSuite()
    t= unittest.TextTestRunner(verbosity=2)
    t.run( suite5 )

    我们基于当前模块的doctest字符串创建了一个组件,suite5。我们在这个组件上使用了unittestTextTestRunner。另一种方式是,可以将doctest组件与其他TestCases相结合来创建更大、更完整的组件。

    15.2.2 创建一个更完整的测试包

    对于大型应用来说,每一个应用模块中可以包含一个单独的模块,存放模块中的TestCases。可以并行地使用两种包结构:在应用模块中使用src结构,在测试模块中使用test结构。以下两种目录树展示了这些模块的结构。

    src
       init.py
       main.py
       module1.py
       module2.py
       setup.py
    test
       init.py
       module1.py
       module2.py
       all.py

    可以看到,两种结构并不是完全并行的。通常不会为setup.py包含一个自动化单元测试。一个良好设计的 main .py可能不需要一个单独的单元测试,因为它不应该包含太多代码。在第16章“使用命令行”中,会详细介绍几种设计 main .py的方式。

    我们可以创建一个最上层的test/all.py模块,将所有这些测试放进一个组件中。

    import module1
    import module2
    import unittest
    import doctest
    all_tests= unittest.TestSuite()
    for mod in module1, module2:
       all_tests.addTests( mod.suite() )
       all_tests.addTests( doctest.DocTestSuite(mod) )
    t= unittest.TextTestRunner()
    t.run( all_tests )

    我们基于其他测试模块中的组件创建了一个单独的组件 all _ tests,这样就可以使用一个方便的脚本来运行有效的测试。

    还有几种可以使用unittest模块中测试查找功能的方法达到同样的目的。我们将从命令行来执行包级别的测试,如以下代码所示。

    python3.3 -m unittest test / *.py

    这将使用unittest中的默认测试查找功能来对指定文件中的TestCases进行查找。这样做的缺点是,会依赖于shell脚本功能,而非纯Python功能。有时通配符文件规范使得开发更复杂,因为未完成的模块可能会被测试。