使用 py.test 和 mock
Python 标准库提供的测试模块功能相对单一,所以在项目中通常会额外使用第三方的测试工具。
py.test
py.test(https://pytest.org )也叫作 pytest,除了比 Python 标准的单元测试模块 unittest 更简洁和高效外,还有如下特点:
- 容易上手,入门简单,文档中有很多实例可以参考。
- 可以自动发现需要测试的模块和函数。
- 支持运行由 nose、unittest 等模块编写的测试用例。
- 有很多第三方插件,并且可以方便地自定义插件。
- 很容易与持续集成工具结合。
- 可以细粒度地控制要测试的测试用例。
我们先安装它:
> pip install pytest
下面演示 py.test 常用的测试方法(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):
# py.test 不需要使用 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 类中,py.test 只要求函数或者类以 test 开头即可。
运行一下:
> py.test chapter8/section2/test_pytest.py
====================test session starts======================
platform linux2 -- Python 2.7.11 -- py-1.4.31 -- pytest-2.6.4
plugins:django
collected 8 items
chapter8/section2/test_pytest.py ........
====================8 passed in 4.04 seconds=================
测试通过了。我们让其中一个测试失败:
> py.test
====================test session starts======================
platform linux2 -- Python 2.7.6 -- py-1.4.31 -- pytest-2.6.4
plugins:django
collected 8 items
test_pytest.py ...F....
====================FAILURES=================================
__________________TestClass.test_two__________________________
self=<test_pytest.TestClass object at 0x7fe8de0ba210>
setup_math=<module 'math' (built-in)>
def test_two(self, setup_math):
> assert setup_math.ceil(10)==11.0
E assert 10.0==11.0
E + where 10.0=<built-in function ceil>(10)
E + where<built-in function ceil>=<module 'math' (built-in)>.ceil
test_pytest.py:34:AssertionError
=============1 failed, 7 passed in 0.12 seconds==============
py.test 帮助我们定位到测试失败的位置,并告诉我们预期值和实际值。py.test 的命令行功能非常丰富:
# 和使用 py.test 的作用一样
> python-m pytest chapter8/section2/test_pytest.py
# 验证整个目录
> py.test chapter8/section2
# 只验证文件中单个测试用例,这在实际工作中非常方便,否则可能需要运行一段时间才
能轮到有问题的测试用例,极为浪费时间。使用这样的方式就可以有针对性地验证有
问题的测试用例
> py.test chapter8/section2/test_pytest.py::test_mytest
# 只验证测试类中的单个方法
> py.test chapter8/section2/test_pytest.py::TestClass::test_in
插件
py.test 有丰富的第三方插件,如下 3 个插件很有用。
1.pytest-random:让测试用例的测试顺序变成随机的。当有很多测试用例时,这个插件不会让测试只卡在一个异常上,有助于发现其他异常。
> pip install pytest-random # 插件的名字都以`pytest-`开头 > py.test --random
2.pytest-xdist:让 py.test 支持分布式的测试。
> pip install pytest-xdist
# 把测试分发到多个 CPU 上执行,如果测试花费的时间太久以及需要大量 I/O 操作,
使用多个 CPU 能减少很多时间
> py.test -n 3 chapter8/section2/test_pytest.py
# 使用子进程进行并发测试
> py.test -d --tx 3\*popen//python=python2.7 chapter8/section2/test_pytest.py
3.pytest-instafail:一旦测试出现出错信息就立即返回,不需要等全部测试结束后才显示。
> pip install pytest-instafail > py.test --instafail
你可能听过或者在用 Nose,它最近几年一直处于维护状态而且可能停止,应该使用 py.test 替换掉它。
mock
Mock 测试是在测试过程中对可能不稳定、有副作用、不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便完成测试的方法。在 Python 中这种测试是通过第三方的 mock 库完成的,mock 在 Python 3.3 的时候被引入到 Python 标准库中,改名为 unittest.mock。之前的 Python 版本都需要安装它:
> pip install mock
假设现在一个单元测试依赖外部的 API 返回的值。举个例子(client.py):
import requests
def api_request(url):
r=requests.get(url)
return r.json()
def get_review_author(url):
rs=api_request(url)
return rs['review']['author']
如果测试时每次都真正去请求这个接口,就会有两个问题:
- 测试环境可能和线上环境不同,需要搭建本地的 API 服务,尤其是需要本地环境能返回线上环境实际的全部结果,增加复杂度且效率低下。
- 测试结果严重依赖外部 API 服务的稳定性。
使用 mock 的解决方案如下(test_mock.py):
import unittest
import mock
import client
class TestClient(unittest.TestCase):
def setUp(self):
self.result={'review':{'author':'dongwm'}}
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.dongwm.com/review/123'), 'dongwm')
这个测试并没有实际地请求 API 就达到了测试的目的。还可以通过 mock.patch 来创建:
def test_request(self):
api_result=mock.Mock(return_value=self.result)
with mock.patch('client.api_request', api_result):
self.assertEqual(client.get_review_author(
'http://api.dongwm.com/review/123'), 'dongwm')
使用 patch 的目的是为了控制 mock 的范围,使其在 patch 上下文之外不受影响,这样使用更灵活。mock.patch 除了用作上下文管理器,还可以作为装饰器加在测试方法上:
@mock.patch('client.api_request')
def test_request(self, api_request): # 每个 patch 装饰器会作为一个参数传进来
api_request.return_value=self.result
self.assertEqual(client.get_review_author(
'http://review.dongwm.com/dongwm'), 'dongwm')
当初始化 mock.Mock 类之后,可以对任何属性及其子属性进行预设。另一个常见的模拟场景是数据库操作。下面模拟 MySQL 的查询语句:
In : mock=Mock()
In : cursor=mock.connection.cursor.return_value
In : cursor.execute.return_value=(1, 'xiaoming')
In : mock.connection.cursor().execute('SELECT * from users limit 1')
Out : (1, 'xiaoming')
In : mock.connection.cursor().execute('show tables') # 无论参数是什么,都会返回这个
值
Out : (1, 'xiaoming')
当被模拟的函数(方法)有参数且参数对返回值有影响,或者抛出异常时,使用 side_effect:
def test_side_effect():
mock=Mock()
def effect(*args,**kwargs):
raise IndexError
mock.side_effect=effect
with pytest.raises(IndexError):
mock(1, 2, a=3)
side_effect=lambda value, length=1:value * length
mock.side_effect=side_effect
assert mock(1)==1
assert mock('*', 2)=='**'
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论