Python测试应用与公具 今天跟大家分享一个Python与测试相关的话题,主要介绍Python中的标准库
unittest
及第三方测试工具pytest
及mock
。介绍了它们的基本使用。
unittest是Python标准库中用于单元测试的模块。单元测试用来对最小可测试单元进行正确性检验,帮助我们在上线之前发现问题。
接下来我们通过测试collections模块中的Counter类,先来了解unittest的用法。大家或许对collections库中Counter类不太熟悉,为了让大家更好地理解这个例子,这里简单介绍一下Counter的使用。
>>> from collections import Counter >>> c = Counter('abcdaba') # 用来计算字符串abcdaba中各个字符出现的次数 >>> c.keys() dict_keys(['a', 'b', 'c', 'd']) >>> c.values() dict_values([3, 2, 1, 1]) >>> c.elements() <bound method Counter.elements of Counter({'a': 3, 'b': 2, 'c': 1, 'd': 1})> >>> c['a'] 3 >>> c['b'] 2 >>> c['c'] 1 >>> c['d'] 1
一个测试脚本:
# filename: ut_case.py import unittest from collections import Counter class TestCounter(unittest.TestCase): def setUp(self): self.c = Counter('abcdaba') print('setUp starting...') def test_basics(self): c = self.c self.assertEqual(c, Counter(a=3, b=2, c=1, d=1)) self.assertIsInstance(c, dict) self.assertEqual(len(c), 4) self.assertIn('a', c) self.assertNotIn('f', c) self.assertRaises(TypeError, hash, c) def test_update(self): c = self.c c.update(f=1) self.assertEqual(c, Counter(a=3, b=2, c=1, d=1, f=1)) c.update(a=10) self.assertEqual(c, Counter(a=13, b=2, c=1, d=1, f=1)) def tearDown(self): print('tearDown starting...') if __name__ == '__main__': unittest.main()
setUp
方法列出了测试前的准备工作,常用来做一些初始化的工作,非必需方法。tearDown
方法列出了测试完成后的收尾工作,用来销毁测试过程中产生的影响,也是非必需方法。TestCase
,顾名思义表示测试用例,一个测试用例可以包含多个测试方法,每个测试方法都要以test_
开头。测试方法中用到的self.assertXXX
方法是断言语句,单元测试都是使用这样的断言语句判断测试是否通过的:如果断言为False
,会抛出AssertionError
异常,测试框架就会认为此测试用例测试失败。
运行一下上面的脚本:
(venv) C:\Users\LavenLiu\IdeaProjects\TestOps>python ut_case.py setUp starting... tearDown starting... .setUp starting... tearDown starting... . ---------------------------------------------------------------------- Ran 2 tests in 0.003s OK
可以看到每次执行test_
开头的方法时,都会执行setUp
和tearDown
。
Python标准库提供的测试模块功能相对单一,所以在项目中通常会额外使用第三方的测试工具。这里我们介绍pytest,pytest除了比Python标准的单元测试模块unittest更简洁和高效外,还有如下特点:
我们要先安装pytest库:
pip install pytest
接下来演示pytest常用的测试方法。一个测试用例:
# filename: test_pytest.py import pytest @pytest.fixture # 创建测试环境,可以用来做setUp和tearDown的工作 def setup_math(): import math return math @pytest.fixture(scope='function') def setup_function(request): def teardown_function(): print('teardown_function called.') request.addfinalizer(teardown_function) # 这个内嵌函数做tearDown工作 print('setup_function called.') def test_func(setup_function): print('Test_Func called.') def test_setup_math(setup_math): # pytest不需要使用self.assertXXX这样的方法,直接使用Python内置的assert断言语句即可 assert setup_math.pow(2, 3) == 8.0 class TestClass(object): def test_in(self): assert 'h' in 'hello' def test_two(self, setup_math): assert setup_math.ceil(10) == 10.0 def raise_exit(): raise SystemExit(1) def test_mytest(): with pytest.raises(SystemExit): raise_exit() @pytest.mark.parametrize('test_input, expected', [ ('1+3', 4), ('2*4', 8), ('1==2', False), ]) # parametrize可以用装饰器的方式集成多组测试用例 def test_eval(test_input, expected): assert eval(test_input) == expected
unittest必须把测试放在TestCase类中,pytest只要求测试函数或者类以test开头即可。运行一下上面的脚本:
(venv) C:\Users\LavenLiu\IdeaProjects\TestOps>py.test test_pytest.py ============================= test session starts ============================= platform win32 -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 rootdir: C:\Users\LavenLiu\IdeaProjects\TestOps, inifile: plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2 collected 12 items test_pytest.py ............ ========================== 12 passed in 0.05 seconds ==========================
测试通过,我们让其中一个测试用例测试失败:
(venv) C:\Users\LavenLiu\IdeaProjects\TestOps>py.test test_pytest.py ============================= test session starts ============================= platform win32 -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 rootdir: C:\Users\LavenLiu\IdeaProjects\TestOps, inifile: plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2 collected 8 items test_pytest.py ...F.... ================================== FAILURES =================================== _____________________________ TestClass.test_two ______________________________ self = <test_pytest.TestClass object at 0x0000000003837588> setup_math = <module 'math' (built-in)> def test_two(self, setup_math): > assert setup_math.ceil(10) == 11.0 E AssertionError: assert 10 == 11.0 E + where 10 = <built-in function ceil>(10)E + where <built-in function ceil> = <module 'math' (built-in)>.ceil test_pytest.py:31: AssertionError ===================== 1 failed, 7 passed in 0.08 seconds ======================
pytest帮助我们定位到测试失败的位置,并告诉我们预期值和实际值。pytest的命令行功能非常丰富:
# 与使用pytest的作用一样 python -m pytest test_pytest.py # 验证整个目录 pytest /path/to/test/dir # 只验证文件中的单个测试用例,这在实际工作中非常方便, # 否则可能需要运行一段时间才能轮到有问题的测试用例,极为浪费时间。 # 使用这样的方式就可以有针对性地验证有问题的测试用例 pytest test_pytest.py::test_mytest # 只验证测试类中的单个方法 pytest test_pytest.py::TestClass::test_in
pytest有丰富的插件,这里列出几个常用的pytest插件,pytest插件都是以pytest-
开头。
pytest-random
:可以让测试变得随机。当有很多测试用例时,这个插件不会让测试只卡在一个异常上,有助于发现其他异常。pytest-xdist
:让pytest支持分布式测试pytest-instafail
:一旦出现错误信息就立即返回,不需要等到全部测试结束后才显示。pytest-html
:可以生存测试报告文件。Mock测试是在测试过程中对可能不稳定、有副作用、不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便完成测试的方法。在Python中,这种测试是通过第三方的mock
库完成的,mock
在Python3.3的时候被引入到了Python标准库中,改名为unittest.mock
。之前的Python版本都需要安装它:
pip install mock
假设现在一个单元测试依赖外部的API返回值。举个例子(client.py):
# filename: client.py import requests def api_request(url): r = requests.get(url) return r.json() def get_review_author(url): res = api_request(url) return res['review']['author']
如果在测试时,每次都真正请求这个接口,就会有两个问题:
使用mock
的解决方案如下(test_mock.py):
# filename: test_mock.py import unittest import mock import client class TestClient(unittest.TestCase): def setUp(self): self.result = {'review': {'author': 'testops'}} def test_request(self): api_result = mock.Mock(return_value=self.result) client.api_request = api_result self.assertEqual(client.get_review_author( 'http://api.testops.cn/review/123'), 'testops')
运行一下看看效果:
(venv) C:\Users\LavenLiu\IdeaProjects\TestOps>pytest test_mock_py3.py ============================= test session starts ============================= rootdir: C:\Users\LavenLiu\IdeaProjects\TestOps, inifile: plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2 collected 1 item test_mock_py3.py . ========================== 1 passed in 0.36 seconds ===========================
可以看到,这个测试并没有实际地请求API就达到了测试的目的。
好的,今天就介绍这么多,后续会继续更新。请大家多多关注。
本文分享自微信公众号 - 小白的技术客栈(XBDJSKZ),作者:lavenliu.cn
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2017-11-14
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句