我编写的 Tombola 示例测试脚本用到两个类属性,用它们内省类的继承关系。

__subclasses__()

  这个方法返回类的直接子类列表,不含虚拟子类。

_abc_registry

  只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子类的弱引用。

为了测试 Tombola 的所有子类,我编写的脚本迭代 Tombola.__subclasses__()Tombola._abc_registry 得到的列表,然后把各个类赋值给在 doctest 中使用的 ConcreteTombola

这个测试脚本成功运行时输出的结果如下:

$ python3 tombola_runner.py
BingoCage        24 tests,  0 failed - OK
LotteryBlower    24 tests,  0 failed - OK
TumblingDrum     24 tests,  0 failed - OK
TomboList        24 tests,  0 failed - OK

测试脚本的代码在示例 11-15 中,doctest 在示例 11-16 中。

示例 11-15 tombola_runner.py:Tombola 子类的测试运行程序

import doctest

from tombola import Tombola

# 要测试的模块
import bingo, lotto, tombolist, drum  ➊

TEST_FILE = 'tombola_tests.rst'
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'


def main(argv):
    verbose = '-v' in argv
    real_subclasses = Tombola.__subclasses__()  ➋
    virtual_subclasses = list(Tombola._abc_registry)  ➌

    for cls in real_subclasses + virtual_subclasses:  ➍
        test(cls, verbose)


def test(cls, verbose=False):

    res = doctest.testfile(
            TEST_FILE,
            globs={'ConcreteTombola': cls},  ➎
            verbose=verbose,
            optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
    tag = 'FAIL' if res.failed else 'OK'
    print(TEST_MSG.format(cls.__name__, res, tag))  ➏

if __name__ == '__main__':
    import sys
    main(sys.argv)

❶ 导入包含 Tombola 真实子类和虚拟子类的模块,用于测试。

__subclasses__() 返回的列表是内存中存在的直接子代。即便源码中用不到想测试的模块,也要将其导入,因为要把那些类载入内存。

❸ 把 _abc_registryWeakSet 对象)转换成列表,这样方能与 __subclasses__() 的结果拼接起来。

❹ 迭代找到的各个子类,分别传给 test 函数。

❺ 把 cls 参数(要测试的类)绑定到全局命名空间里的 ConcreteTombola 名称上,供 doctest 使用。

❻ 输出测试结果,包含类的名称、尝试运行的测试数量、失败的测试数量,以及 'OK''FAIL' 标记。

doctest 文件如示例 11-16 所示。

示例 11-16 tombola_tests.rst:Tombola 子类的 doctest

==============
Tombola tests
==============

Every concrete subclass of Tombola should pass these tests.


Create and load instance from iterable::

    >>> balls = list(range(3))
    >>> globe = ConcreteTombola(balls)
    >>> globe.loaded()
    True
    >>> globe.inspect()
    (0, 1, 2)


Pick and collect balls::

    >>> picks = []
    >>> picks.append(globe.pick())
    >>> picks.append(globe.pick())
    >>> picks.append(globe.pick())


Check state and results::

    >>> globe.loaded()
    False
    >>> sorted(picks) == balls
    True


Reload::

    >>> globe.load(balls)
    >>> globe.loaded()
    True
    >>> picks = [globe.pick() for i in balls]
    >>> globe.loaded()
    False


Check that `LookupError` (or a subclass) is the exception
thrown when the device is empty::

    >>> globe = ConcreteTombola([])
    >>> try:
    ... globe.pick()
    ... except LookupError as exc:
    ... print('OK')
    OK


Load and pick 100 balls to verify that they all come out::

    >>> balls = list(range(100))
    >>> globe = ConcreteTombola(balls)
    >>> picks = []
    >>> while globe.inspect():
    ... picks.append(globe.pick())
    >>> len(picks) == len(balls)
    True
    >>> set(picks) == set(balls)
    True


Check that the order has changed and is not simply reversed::
    >>> picks != balls
    True
    >>> picks[::-1] != balls
    True

Note: the previous 2 tests have a *very* small chance of failing
even if the implementation is OK. The probability of the 100
balls coming out, by chance, in the order they were inspect is
1/100!, or approximately 1.07e-158. It's much easier to win the
Lotto or to become a billionaire working as a programmer.

THE END

我们对 Tombola 抽象基类的分析到此结束。下一节说明 Python 如何使用抽象基类的 register 函数。