《 Python之路》一书。 开发,扩展,测试和部署的黑带”

图片 嗨,habrozhiteli! Python路径使您可以磨练专业技能并尽可能多地了解最流行的编程语言的功能。 您将学习如何编写有效的代码,在最短的时间内创建最佳的程序并避免常见的错误。 现在是时候了解多线程计算和备注,获取API和数据库设计领域的专家意见,以及深入了解Python来加深对语言的了解的时候了。 您必须启动一个项目,使用版本,组织自动测试并为特定任务选择编程样式。 然后,您将继续研究有效的函数声明,选择合适的数据结构和库,创建无故障的程序,程序包并在字节码级别上优化程序。

摘录。 并行运行测试


运行测试套件可能很耗时。 当测试套件需要几分钟的时间才能完成时,这在大型项目中很常见。 默认情况下,pytest按特定顺序顺序运行测试。

由于大多数计算机都具有多核处理器,因此,如果您将测试分开以在多核上运行,则可以加快速度。

为此,pytest有一个pytest-xdist插件,可以使用pip进行安装。 该插件使用––numprocesses(缩写为–n)参数扩展了pytest命令行,该参数将用作参数的内核数。 启动pytest –n 4将在四个并行进程中运行测试套件,从而在可用内核负载之间保持平衡。

由于内核数量可能会有所不同,因此插件也接受auto关键字作为值。 在这种情况下,可用内核数将自动返回。

使用夹具创建测试中使用的对象


在单元测试中,通常需要在运行测试之前和之后执行一组标准操作,并且这些说明涉及某些组件。 例如,您可能需要一个对象来表达应用程序配置的状态,并且必须在每次测试之前将其初始化,然后在执行后将其重置为初始值。 同样,如果测试依赖于临时文件,则必须在测试之前创建此文件,然后在测试之后删除。 这些组件称为固定装置 。 它们在测试之前已安装,并在执行后消失。

在pytest中,fixture被声明为简单函数。 夹具函数必须返回所需的对象,以便在测试使用它的位置时可以使用该对象。

这是一个简单的夹具示例:

import pytest @pytest.fixture def database(): return <some database connection> def test_insert(database): database.insert(123) 

列表中包含数据库参数的任何测试都会自动使用数据库固定装置。 test_insert()函数将接收database()函数的结果作为第一个参数,并将在认为合适时使用此结果。 通过使用固定装置,您无需重复多次数据库初始化代码。

代码测试的另一个共同特征是在夹具操作后能够去除多余的东西。 例如,关闭数据库连接。 将夹具实现为生成器将添加用于清理已验证对象的功能(清单6.5)。

清单6.5。 清除已验证的对象


 import pytest @pytest.fixture def database(): db = <some database connection> yield db db.close() def test_insert(database): database.insert(123) 
由于我们使用了yield关键字并从数据库中生成了一个生成器,所以yield语句之后的代码仅在测试结束时执行。 该代码将在测试结束时关闭数据库连接。

对于每个测试,关闭数据库连接可能会导致不合理的计算能力浪费,因为其他测试可能会使用已经打开的连接。 在这种情况下,您可以将scope参数传递给灯具装饰器,并指定其范围:

 import pytest @pytest.fixture(scope="module") def database(): db = <some database connection> yield db db.close() def test_insert(database): database.insert(123) 

通过指定scope =“ module”参数,您可以为整个模块初始化一次固定装置,现在所有请求它的测试功能都可以使用开放式数据库连接。

您可以在测试之前或之后运行一些常规代码,将固定装置定义为与autouse关键字一起自动使用,而不是将它们指定为每个测试功能的参数。 使用True参数(即autouse关键字)对pytest.fixture()函数进行具体化,可确保每次在声明了模块或类的测试中运行测试之前都调用Fixture。

 import os import pytest @pytest.fixture(autouse=True) def change_user_env(): curuser = os.environ.get("USER") os.environ["USER"] = "foobar" yield os.environ["USER"] = curuser def test_user(): assert os.getenv("USER") == "foobar"</source     .  ,    :     ,      ,       . <h3>  </h3>           ,    ,   ,         .          Gnocchi,    . Gnocchi      <i>storage API</i>.    Python          .       ,      API   .        ,      (    storage API),  ,       .   ,   <i> </i>,     ,        .  6.6          ,    :    mysql,   —  postgresql. <blockquote><h4> 6.6.      </h4> <source lang="python">import pytest import myapp @pytest.fixture(params=["mysql", "postgresql"]) def database(request): d = myapp.driver(request.param) d.start() yield d d.stop() def test_insert(database): database.insert("somedata") 
驱动程序固定装置接收两个不同的值作为参数-应用程序支持的数据库驱动程序的名称。 test_insert运行两次:一次用于MySQL数据库,第二次用于PostgreSQL数据库。 这使得在不添加新代码行的情况下,轻松进行相同的测试,但具有不同的场景。

虚拟对象的托管测试


虚拟对象(或存根,模拟对象)是模仿真实应用程序对象行为的对象,但处于特殊的受控状态。 它们在创建能够全面描述测试条件的环境中最有用。 您可以用虚拟对象替换除测试对象以外的所有对象,并隔离它们,并创建代码测试环境。

一种用例是创建一个HTTP客户端。 创建HTTP服务器几乎是不可能的(或者说是极其困难的),在该服务器上可以为每个可能的值运行所有情况和方案。 HTTP客户端特别难以测试错误情况。

标准库有一个模拟命令来创建虚拟对象。 从Python 3.3开始,mock已与unittest.mock库集成。 因此,您可以使用下面的代码段在Python 3.3和更早版本之间提供向后兼容性:

 try: from unittest import mock except ImportError: import mock 

模拟库非常易于使用。 可用于嘲笑对象的任何属性都是在运行时动态创建的。 可以为任何属性分配任何值。 在清单6.7中,模拟用于为dummy属性创建一个哑对象。

清单6.7。 访问mock.Mock属性


 >>> from unittest import mock >>> m = mock.Mock() >>> m.some_attribute = "hello world" >>> m.some_attribute "hello world" 
您还可以为可变对象动态创建一个方法,如清单6.8所示,在该方法中,您将创建一个始终返回42并将所需的内容作为参数的虚拟方法。

清单6.8 为嘲笑模拟对象创建方法


 >>> from unittest import mock >>> m = mock.Mock() >>> m.some_method.return_value = 42 >>> m.some_method() 42 >>> m.some_method("with", "arguments") 42 
现在只有两行,并且mock.Mock对象现在具有some_method()方法,该方法返回42。它接受任何类型的参数,而没有验证该参数是什么。

动态生成的方法也可能具有(故意)副作用。 为了不仅仅是返回值的样板方法,可以将它们定义为执行有用的代码。

清单6.9创建了一个具有副作用的虚拟方法-它显示字符串“ hello world”。

清单6.9。 创建带有副作用的模拟对象的方法


  >>> from unittest import mock >>> m = mock.Mock() >>> def print_hello(): ... print("hello world!") ... return 43 ... ❶ >>> m.some_method.side_effect = print_hello >>> m.some_method() hello world! 43 ❷ >>> m.some_method.call_count 1 
我们将整个函数分配给some_method❶属性。 从技术上讲,由于您可以在虚拟对象中包括测试所需的任何代码,因此,这允许您在测试中实现更复杂的方案。 接下来,您需要将此对象传递给期望它的函数。

❷call_count属性是一种检查方法被调用次数的简便方法。

模拟库使用“动作检查”模式:这意味着在测试后,您需要确保正确执行了由假人替换的动作。 清单6.10将assert()方法应用于虚拟对象以执行这些检查。

清单6.10 通话验证方式


  >>> from unittest import mock >>> m = mock.Mock() ❶ >>> m.some_method('foo', 'bar') <Mock name='mock.some_method()' id='26144272'> ❷ >>> m.some_method.assert_called_once_with('foo', 'bar') >>> m.some_method.assert_called_once_with('foo', ❸mock.ANY) >>> m.some_method.assert_called_once_with('foo', 'baz') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/dist-packages/mock.py", line 846, in assert_cal led_once_with return self.assert_called_with(*args, **kwargs) File "/usr/lib/python2.7/dist-packages/mock.py", line 835, in assert_cal led_with raise AssertionError(msg) AssertionError: Expected call: some_method('foo', 'baz') Actual call: some_method('foo', 'bar') 
我们通过调用❶方法创建了带有foo和bar参数作为测试的方法。 测试对伪对象的调用的一种简单方法是使用assert_drawn()方法,例如assert_drawn_once_with()❷。 对于这些方法,您需要在调用虚拟方法时传递希望使用的值。 如果传递的值与使用的值不同,则模拟程序会引发AssertionError异常。 如果您不知道可以传递什么参数,请使用mock.ANY作为❸的值。 它将替换传递给dummy方法的所有参数。

模拟库还可以用于替换外部模块中的函数,方法或对象。 在清单6.11中,我们用自己的伪函数替换了os.unlink()函数。

清单6.11 使用嘲笑


 >>> from unittest import mock >>> import os >>> def fake_os_unlink(path): ... raise IOError("Testing!") ... >>> with mock.patch('os.unlink', fake_os_unlink): ... os.unlink('foobar') ... Traceback (most recent call last): File "<stdin>", line 2, in <module> File "<stdin>", line 2, in fake_os_unlink IOError: Testing! 
当用作上下文管理器时,mock.patch()将目标函数替换为我们选择的目标函数。 这是必需的,以便在上下文中执行的代码使用更正的方法。 使用嘲笑(。)方法,您可以修改外部代码的任何部分,以使其能够测试应用程序的所有条件的方式工作(清单6.12)。

清单6.12 使用mock.patch()测试许多行为


  from unittest import mock import pytest import requests class WhereIsPythonError(Exception): passdef is_python_still_a_programming_language(): try: r = requests.get("http://python.org") except IOError: pass else: if r.status_code == 200: return 'Python is a programming language' in r.content raise WhereIsPythonError("Something bad happened") def get_fake_get(status_code, content): m = mock.Mock() m.status_code = status_code m.content = content def fake_get(url): return m return fake_get def raise_get(url): raise IOError("Unable to fetch url %s" % url) ❷ @mock.patch('requests.get', get_fake_get( 200, 'Python is a programming language for sure')) def test_python_is(): assert is_python_still_a_programming_language() is True @mock.patch('requests.get', get_fake_get( 200, 'Python is no more a programming language')) def test_python_is_not(): assert is_python_still_a_programming_language() is False @mock.patch('requests.get', get_fake_get(404, 'Whatever')) def test_bad_status_code(): with pytest.raises(WhereIsPythonError): is_python_still_a_programming_language() @mock.patch('requests.get', raise_get) def test_ioerror(): with pytest.raises(WhereIsPythonError): is_python_still_a_programming_language() 


清单6.12实现了一个测试用例,该用例查找python.org上 的编程语言字符串来查找Python的所有实例。 在测试中不会在所选网页上找不到任何给定的行的选项。 要获得否定的结果,您需要更改页面,但这不能完成。 但是,在模拟的帮助下,您可以使用技巧来更改请求的行为,以使其返回带有虚拟页面的虚拟响应,该页面不包含给定的字符串。 这将允许您测试python.org不包含给定字符串的否定情况,并确保程序正确处理了这种情况。

本示例使用了mock.patch()装饰器版本。 虚拟对象的行为不会改变,并且在测试功能的上下文中设置示例更加容易。

使用虚拟对象将帮助模拟任何问题:服务器返回404错误,I / O错误或网络延迟错误。 我们可以确保代码在每种情况下都返回正确的值或引发正确的异常,从而保证了代码的预期行为。

确定涵盖范围的未经测试的代码


覆盖率工具是对单元测试的一个重要补充[代码覆盖率是测试中使用的一种度量。 显示在测试过程中执行的程序源代码的百分比 ],查找未测试的代码段。 它使用代码分析和跟踪工具来识别已执行的行。 在单元测试中,它可以揭示代码的哪些部分被重用,哪些部分根本没有使用。 创建测试是必要的,并且能够找出您忘记用测试覆盖的代码的哪一部分,使此过程更加有趣。

通过pip安装coverage模块,以便可以在您的外壳中使用它。

注意


如果通过操作系统的安装程序安装了该模块,则该命令也可以称为python-coverage。 例如Debian OS。


离线使用覆盖非常简单。 它显示了程序中永远不会启动并变成“死重”的部分-这样的代码,您必须在不更改程序功能的情况下将其删除。 本章前面讨论的所有测试工具都与coverage集成在一起。

使用pytest时,请通过pip install pytest-pycov安装pytest-cov插件,并添加一些开关以生成未测试代码的详细输出(清单6.13)。

清单6.13 使用pytest和coverage


 $ pytest --cov=gnocchiclient gnocchiclient/tests/unit ---------- coverage: platform darwin, python 3.6.4-final-0 ----------- Name Stmts Miss Branch BrPart Cover --------------------------- gnocchiclient/__init__.py 0 0 0 0 100% gnocchiclient/auth.py 51 23 6 0 49% gnocchiclient/benchmark.py 175 175 36 0 0% --snip-- --------------------------- TOTAL 2040 1868 424 6 8% === passed in 5.00 seconds === 
--cov选项可在测试结束时启用覆盖率报告的输出。 您必须将软件包名称作为参数传递,以便插件正确过滤报告。 输出将包含尚未执行的代码行,这意味着它们尚未经过测试。 剩下的就是打开编辑器并为此代码编写测试。

Coverage模块甚至更好-它使您可以生成HTML格式的清晰报告。 只需添加-cov-report-html,HTML页面将出现在运行命令的htmlcov目录中。 每个页面将显示源代码的哪些部分正在运行或未运行。

如果您想走得更远,请使用–-cover-fail-under-COVER_MIN_PERCENTAGE,如果未覆盖最低百分比的代码,这将导致测试套件失败。 尽管覆盖率很大是一个不错的目标,并且测试工具可用于获取有关测试覆盖率状态的信息,但该百分比本身并不是很有用。 图6.1显示了覆盖率报告示例,其中显示了覆盖率百分比。

例如,用100%的测试覆盖代码是一个值得的目标,但这并不一定意味着代码已经过全面测试。 该值仅表示程序中所有代码行均已满足,但并不表示所有条件均已测试。

值得使用覆盖率信息来扩展测试套件并为未运行的代码创建它们。 这样可以简化项目支持并提高整体代码质量。

图片


关于作者


Julien Danju入侵免费软件已有20年之久,并且已经开发Python程序已有近12年的时间。 他目前领导基于OpenStack的分布式云平台的设计团队,该平台拥有现有的最大的Python开源数据库,大约有两百五十万行代码。 在开发云服务之前,Julien创建了窗口管理器并为许多项目的开发做出了贡献,例如Debian和GNU Emacs。

关于科学编辑


Mike Driscoll从事Python编程已有十多年了。 很长一段时间,他在The Mouse vs. Python 。 撰写多本Python书籍:Python 101,Python访谈和ReportLab:使用Python进行PDF处理。 您可以在Twitter和GitHub上找到Mike:@driscollis。

»这本书的更多信息可以在出版商的网站上找到
» 目录
» 摘录

小贩优惠券25%优惠-Python

支付纸质版本的书后,就会通过电子邮件发送电子书。

Source: https://habr.com/ru/post/zh-CN466027/


All Articles