
代码覆盖率code coverage是一个非常有用的度量标准它提供了有关项目代码的测试的客观信息。它仅仅是在所有测试执行期间执行多少和哪些代码行的测量。它通常表示为百分比100%覆盖意味着每个代码行都在测试期间执行。最流行的代码覆盖工具被称为 simply coverage并在 PyPI 上免费提供。使用非常简单只包括两个步骤。第一步是在 shell 中运行 coverage run 命令并将脚本/程序的路径作为参数运行所有测试$ coverage run --source .which py.test-v test session starts platformdarwin – Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1 –/Users/swistakm/.envs/book/bin/python3cachedir: .cacherootdir: /Users/swistakm/dev/book/chapter10/pytest, inifile:plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0collected 6 itemsprimes.py::pyflakes PASSEDprimes.py::pep8 PASSEDtest_primes.py::pyflakes PASSEDtest_primes.py::pep8 PASSEDtest_primes.py::test_is_prime_true PASSEDtest_primes.py::test_is_prime_false PASSED 6 passed, 1 pytest-warnings in 0.10 seconds coverage run 命令还接受指定可运行模块名称的-m 参数而不是可能更适合某些测试框架的程序路径如下所示$ coverage run -m unittest$ coverage run -m nose$ coverage run -m pytest下一步是从.coverage 文件中的生成一个可读的代码覆盖率报告。coverage 包支持一些输出格式最简单的只是在终端中打印 ASCII 表格如下所示$ coverage reportName StmtsMiss Coverprimes.py 7 0 100%test_primes.py 16 0 100%TOTAL 23 0 100%还可以生成 HTML 格式的覆盖率报告你可以在 Web 浏览器中浏览$ coverage html此 HTML 报告默认输出到你的工作目录下的 htmlcov/文件夹中。coverage html命令输出的真正优势是你可以浏览你项目中带有注释的源码其中未被测试覆盖的代码会高亮显示你应该记住虽然你应该始终努力确保 100%的测试覆盖率但它绝不能保证代码被完美测试并且没有代码可以中断的地方。这意味着只有在执行期间才能达到每行代码但不一定每个可能的条件都被测试。在实践中确保完全代码覆盖可能相对容易但是确保达到代码的每个分支是非常困难的。这对于具有 if 语句的包含多个组合的函数以及像list/dict/set 生成式这样的特定语言结构的的测试尤其如此。你应该总是关心好的测试覆盖率但你不应该把它的测量作为最好的测试套件的最终答案。仿真与模拟写单元测试的前提是要隔离被测试的代码单元。测试通常将一些数据传入函数或方法并验证其返回值且/或其执行的副作用这主要是为了确保测试。● 涉及应用程序的原子部分可以是函数、方法、类或接口。● 提供确定的可重现的结果。有时程序组件之间的适当隔离并不明显。例如发送电子邮件的代码它可能会调用 Python 的 smtplib 模块这将通过网络连接与 SMTP 服务器工作。如果我们想要我们的测试是可重现的并且只是测试电子邮件是否具有所需的内容那么这可能无法做到。理想情况下单元测试应该在没有外部依赖性和副作用的计算机上运行。由于 Python 的动态特性可以使用猴子补丁monkey patching在测试固件中修改运行时代码即在运行时动态修改软件而不触及源代码用以仿真fake第三方代码或库的行为。创建一个仿真通过发现测试代码与外部部件一起使用所需的最小交互集可以创建测试中的仿真行为。然后手动返回输出或使用先前已记录的真实数据池。这是通过启动一个空类或函数并将其用作为一个替换来完成的。然后启动测试并且迭代更新仿真直到它正确运行。这可能是由于 Python 类型系统的特性。只要这个对象的行为像期望的类型那它就会被认为与给定的类型兼容并且不需要通过子类化它的祖先。这种在Python 中输入的方法称为鸭子类型——如果某个行为像鸭子一样就可以像鸭子一样对待它。让我们举以下一个例子mailer 模块中调用发送电子邮件的 send 函数import smtplibimport email.messagedef send(sender, to,subject‘None’,body‘None’,server‘localhost’):“”“发送一条信息”“”message email.message.Message()message[‘To’] tomessage[‘From’] sendermessage[‘Subject’] subjectmessage.set_payload(body)server smtplib.SMTP(server)try:return server.sendmail(sender, to, message.as_string())finally:server.quit()相应的测试为from mailer import senddef test_send():res send(‘john.doeexample.com’,‘john.doeexample.com’,‘topic’,‘body’)assert res {}只要本地主机上有 SMTP 服务器此测试将通过并运行。如果没有它会失败像下面这样$ py.test --tbshort test session starts platform darwin – Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1rootdir: /Users/swistakm/dev/book/chapter10/mailer, inifile:plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0collected 5 itemsmailer.py …test_mailer.py …F FAILURES ______________________________ test_send ______________________________test_mailer.py:10: in test_send‘body’mailer.py:19: in sendserver smtplib.SMTP(server)…/smtplib.py:251: ininit(code, msg) self.connect(host, port)…/smtplib.py:335: in connectself.sock self._get_socket(host, port, self.timeout)…/smtplib.py:306: in _get_socketself.source_address)…/socket.py:711: in create_connectionraise err…/socket.py:702: in create_connectionsock.connect(sa)E ConnectionRefusedError: [Errno 61] Connection refused 1 failed, 4 passed, 1 pytest-warnings in 0.17 seconds 可以添加一个补丁来仿真 SMTP 类import smtplibimport pytestfrom mailer import sendclass FakeSMTP(object):passpytest.yield_fixture()def patch_smtplib():设置步骤: smtplib 的猴子补丁old_smtp smtplib.SMTPsmtplib.SMTP FakeSMTPyield卸载步骤: 将 smtplib 恢复到它先前的状态smtplib.SMTP old_smtpdef test_send(patch_smtplib):res send(‘john.doeexample.com’,‘john.doeexample.com’,‘topic’,‘body’)assert res {}在上面的代码中我们使用了一个新的 pytest.yield_fixture()装饰器。它允许我们使用生成器语法在单个固件函数中提供设置和卸载过程。现在我们的测试套件可以使用 smtplib 的修补版本再次运行如下所示$ py.test --tbshort -v test session starts platform darwin – Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1 –/Users/swistakm/.envs/book/bin/python3cachedir: .cacherootdir: /Users/swistakm/dev/book/chapter10/mailer, inifile:plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0collected 5 itemsmailer.py::pyflakes PASSEDmailer.py::pep8 PASSEDtest_mailer.py::pyflakes PASSEDtest_mailer.py::pep8 PASSEDtest_mailer.py::test_send FAILED FAILURES _____________________________ test_send _____________________________test_mailer.py:29: in test_send‘body’mailer.py:19: in sendserver smtplib.SMTP(server)E TypeError: object() takes no parameters 1 failed, 4 passed, 1 pytest-warnings in 0.09 seconds 从上面的文字记录中可以看到我们的 FakeSMTP 类的实现并不完整。我们需要更新其接口以匹配原始的 SMTP 类。根据鸭式类型原则我们只需要提供测试的 send()函数所需的接口如下所示class FakeSMTP(object):definit(self, *args, **kw):这个例子中的参数并不重要passdef quit(self):passdef sendmail(self, *args, **kw):return {}当然仿真类可以随着新的测试的演变来提供更复杂的行为。但它应该尽可能短小和简单。相同的原则可以用于更复杂的输出通过记录它们作为仿真 API 返回。这通常用于第三方服务器例如 LDAP 或 SQL。重要的是要知道当猴子补丁用于任何内置或第三方模块时应该特别注意。如果处理不当这种方法可能会留下不必要的副作用这将在测试之间传播。幸运的是许多测试框架和工具提供了适当的实用程序使任何代码单元的补丁更加安全和容易。在我们的示例中我们手动执行了一切并提供了一个自定义的 patch_smtplib()固件函数具有单独的设置和卸载步骤。py.test 中的典型解决方案要容易得多。这个框架配有一个内置的猴子补丁固件它应该可以满足我们的大部分补丁需求如下所示import smtplibfrom mailer import sendclass FakeSMTP(object):definit(self, *args, **kw):这个例子中的参数并不重要passdef quit(self):passdef sendmail(self, *args, **kw):return {}def test_send(monkeypatch):monkeypatch.setattr(smtplib, ‘SMTP’, FakeSMTP)res send(‘john.doeexample.com’,‘john.doeexample.com’,‘topic’,‘body’)assert res {}你应该知道仿真确实也有局限性。如果你决定仿真一个外部依赖你可能会引入 bug或不必要的行为而真实的服务器不会有反之亦然。使用模拟模拟对象mock objects是可用于隔离所测试代码的通用仿真对象。它们自动化对象的输入和输出的构建过程。在静态类型语言中也大量地使用模拟对象其中猴子补丁更困难但是它们在 Python 中仍然有用可以缩短代码以模仿外部 API。Python 中有很多模拟库但最常见的是 unittest.mock标准库中也提供了该库。创建之初它是一个第三方包并不是 Python 分发版的一部分但很快就作为临时包加入标准库中参考 https://docs.python.org/dev/glossary.html#term-provisional-api。对于 3.3 以前的 Python 版本你需要从 PyPI 安装pip install Mock在我们的示例中使用 unittest.mock 修补 SMTP 比从头开始创建更简单如下所示import smtplibfrom unittest.mock import MagicMockfrom mailer import senddef test_send(monkeypatch):smtp_mock MagicMock()smtp_mock.sendmail.return_value {}monkeypatch.setattr(smtplib, ‘SMTP’, MagicMock(return_valuesmtp_mock))res send(‘john.doeexample.com’,‘john.doeexample.com’,‘topic’,‘body’)assert res {}模拟对象或方法的 return_value 参数允许你定义调用返回的值。当使用模拟对象时每次代码调用某个属性时它都会为该属性即时创建一个新的模拟对象。因此没有抛出异常。这是我们之前写的退出方法的情况例如不需要再定义。在前面的例子中我们实际上创建了两个模拟。● 第一个模拟 SMTP 类对象而不是其实例。这允许你轻松创建新对象而不考虑所期望的__init__()方法。如果被视为可调用Mocks 默认返回新的 Mock()对象。这就是为什么我们需要提供另一个模拟作为其 return_value 关键字参数来控制实例接口。● 第二个模拟的是修补过的 smtplib.SMTP()调用中返回的实际实例。在这个模拟中我们控制 sendmail()方法的行为。在我们的例子中我们使用了 py.test 框架中的 monkey-patching 实用程序但unittest.mock 提供了自己的修补实用程序。在某些情况下例如修补类对象使用它们而不是特定于框架的工具可能更简单并且更快。这里是使用由 unittest.mock 模块提供的 patch()上下文管理器的猴子补丁的示例如下from unittest.mock import patchfrom mailer import senddef test_send():with patch(‘smtplib.SMTP’) as mock:instance mock.return_valueinstance.sendmail.return_value {}res send(‘john.doeexample.com’,‘john.doeexample.com’,‘topic’,‘body’)assert res {}