1、Pytest启动! 1.1 安装 Pytest 使用 pip 进行安装:
1.2 第一个测试函数 Pytest 使用 Python 的 assert
进行条件判断,最简单的测试函数如:
1 2 3 4 def test_passing (): assert (1 , 2 , 3 ) == (1 , 2 , 3 )
1.3 运行测试函数 使用命令 pytest
运行测试函数:
1 2 3 4 5 6 7 8 9 $ pytest tests/test1.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test1.py . [100%] ========================== 1 passed in 0.09 seconds ===========================
pytest 使用 .
标识测试成功(PASSED
)。
可以使用 -v
选项,显示测试的详细信息。
使用 pytest -h
查看 pytest 的所有选项。
1 2 3 4 5 6 7 8 9 ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- c:\anaconda3\python.exe cachedir: .pytest_cache rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests/test1.py::test_passing PASSED [100%] ========================== 1 passed in 0.03 seconds ===========================
1.4 测试失败 下面是一个失败的测试函数:
1 2 3 4 def test_failing (): assert (1 , 2 , 3 ) == (3 , 2 , 1 )
运行结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ pytest tests/test2.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test2.py F [100%] ================================== FAILURES =================================== ________________________________ test_failing _________________________________ def test_failing(): > assert (1, 2, 3) == (3, 2, 1) E assert (1, 2, 3) == (3, 2, 1) E At index 0 diff: 1 != 3 E Use -v to get the full diff tests\test2.py:2: AssertionError ========================== 1 failed in 0.19 seconds ===========================
pytest 使用 F
标识测试失败(FAILED
)。
2、测试函数 2.1 断言 在 pytest 中,assert
是编写测试的最基础工具。如:
1 2 3 assert a == bassert a <= b
2.2 捕获异常 在测试过程中,经常需要测试是否如期抛出预期的异常,以确定异常处理模块生效。在 pytest 中使用 pytest.raises()
进行异常捕获:
1 2 3 4 5 6 7 def test_raises (): with pytest.raises(TypeError) as e: connect('localhost' , '6379' ) exec_msg = e.value.args[0 ] assert exec_msg == 'port type must be int'
运行结果如下:
1 2 3 4 5 6 7 8 9 $ pytest test_raises.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test-function\test_raise.py . [100%] ========================== 1 passed in 0.07 seconds ===========================
2.3 mark函数使用 2.3.1 Pytest 查找测试策略 默认情况下,pytest 会递归查找当前目录下所有以 test
开始或结尾的 Python 脚本,并执行文件内的所有以 test
开始或结束的函数和方法。
对于下面脚本:
1 2 3 4 5 6 7 def test_func1 (): assert 1 == 1 def test_func2 (): assert 1 != 1
直接执行测试脚本会同时执行所有测试函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ pytest tests/test-function/test_no_mark.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests\test-function\test_no_mark.py .F [100%] ================================== FAILURES =================================== _________________________________ test_func2 __________________________________ def test_func2(): > assert 1 != 1 E assert 1 != 1 tests\test-function\test_no_mark.py:6: AssertionError ===================== 1 failed, 1 passed in 0.07 seconds ======================
2.3.2 mark测试函数 由于某种原因(如 test_func2
的功能尚未开发完成),我们只想执行指定的测试函数。在 pytest 中有几种方式可以解决:
第一种,显式指定函数名,通过 ::
标记。
1 2 3 4 5 6 7 8 9 $ pytest tests/test-function/test_no_mark.py::test_func1 ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test-function\test_no_mark.py . [100%] ========================== 1 passed in 0.02 seconds ===========================
第二种,使用模糊匹配,使用 -k
选项标识。
1 2 3 4 5 6 7 8 9 $ pytest -k func1 tests/test-function/test_no_mark.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items / 1 deselected tests\test-function\test_no_mark.py . [100%] =================== 1 passed, 1 deselected in 0.03 seconds ====================
以上两种方法,第一种一次只能指定一个测试函数,当要进行批量测试时无能为力;第二种方法可以批量操作,但需要所有测试的函数名包含相同的模式,也不方便。
第三种,使用 pytest.mark
在函数上进行标记。
带标记的测试函数如:
1 2 3 4 5 6 7 8 9 @pytest.mark.finished def test_func1 (): assert 1 == 1 @pytest.mark.unfinished def test_func2 (): assert 1 != 1
测试时使用 -m
选择标记的测试函数:
1 2 3 4 5 6 7 8 9 $ pytest -m finished tests/test-function/test_with_mark.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items / 1 deselected tests\test-function\test_with_mark.py . [100%] =================== 1 passed, 1 deselected in 0.10 seconds ====================
使用 mark,我们可以给每个函数打上不同的标记,测试时指定就可以允许所有被标记的函数。
一个函数可以打多个标记;多个函数也可以打相同的标记。
运行测试时使用 -m
选项可以加上逻辑,如:
1 2 3 $ pytest -m "finished and commit" $ pytest -m "finished and not merged"
2.4 跳过测试用例 上一节提到 pytest 使用标记过滤测试函数,所以对于那些尚未开发完成的测试,最好的处理方式就是略过而不执行测试。
按正向的思路,我们只要通过标记指定要测试的就可以解决这个问题;但有时候的处境是我们能进行反向的操作才是最好的解决途径,即通过标记指定要跳过的测试。
Pytest 使用特定的标记 pytest.mark.skip
完美的解决了这个问题。
1 2 3 4 5 @pytest.mark.skip(reason='out-of-date api' ) def test_connect (): pass
执行结果可以看到该测试已被忽略:
1 2 3 4 5 6 7 8 9 $ pytest tests/test-function/test_skip.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test-function\test_skip.py s [100%] ========================== 1 skipped in 0.13 seconds ==========================
pytest 使用 s
表示测试被跳过(SKIPPED
)。
Pytest 还支持使用 pytest.mark.skipif
为测试函数指定被忽略的条件。
1 2 3 4 @pytest.mark.skipif(conn.__version__ < '0.2.0' , reason='not supported until v0.2.0' )def test_api (): pass
2.5 预见错误 如果我们事先知道测试函数会执行失败,但又不想直接跳过,而是希望显示的提示。
Pytest 使用 pytest.mark.xfail
实现预见错误功能:
1 2 3 4 5 6 7 8 @pytest.mark.xfail(gen.__version__ < '0.2.0' , reason='not supported until v0.2.0' )def test_api (): id_1 = gen.unique_id() id_2 = gen.unique_id() assert id_1 != id_2
执行结果:
1 2 3 4 5 6 7 8 9 $ pytest tests/test-function/test_xfail.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\test-function\test_xfail.py x [100%] ========================== 1 xfailed in 0.12 seconds ==========================
pytest 使用 x
表示预见的失败(XFAIL
)。
如果预见的是失败,但实际运行测试却成功通过,pytest 使用 X
进行标记(XPASS
)。
2.6 传递参数 当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。
当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。
通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。
在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)
。
这里是一个密码长度的测试函数,其中参数名为 passwd
,其可选列表包含三个值:
1 2 3 4 5 6 7 8 @pytest.mark.parametrize('passwd' , ['123456' , 'abcdefdfs' , 'as52345fasdf4' ] )def test_passwd_length (passwd ): assert len (passwd) >= 8
运行可知执行了三次测试:
1 2 3 4 5 6 7 8 9 $ pytest tests/test-function/test_parametrize.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 3 items tests\test-function\test_parametrize.py F.. [100%] ================================== FAILURES ===================================
再看一个多参数的例子,用于校验用户密码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @pytest.mark.parametrize('user, passwd' , [('jack' , 'abcdefgh' ), ('tom' , 'a123456a' )] )def test_passwd_md5 (user, passwd ): db = { 'jack' : 'e8dc4081b13434b45189a720b77b6818' , 'tom' : '1702a132e769a623c1adb78353fc9503' } import hashlib assert hashlib.md5(passwd.encode()).hexdigest() == db[user]
使用 -v
执行测试
1 2 3 4 5 6 7 8 9 10 11 $ pytest -v tests/test-function/test_parametrize.py::test_passwd_md5 ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- c:\anaconda3\python.exe cachedir: .pytest_cache rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests/test-function/test_parametrize.py::test_passwd_md5[jack-abcdefgh] PASSED [ 50%] tests/test-function/test_parametrize.py::test_passwd_md5[tom-a123456a] PASSED [100%] ========================== 2 passed in 0.04 seconds ===========================
如果觉得每组测试的默认参数显示不清晰,我们可以使用 pytest.param
的 id
参数进行自定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @pytest.mark.parametrize('user, passwd' , [pytest.param('jack' , 'abcdefgh' , id ='User<Jack>' ), pytest.param('tom' , 'a123456a' , id ='User<Tom>' )] )def test_passwd_md5_id (user, passwd ): db = { 'jack' : 'e8dc4081b13434b45189a720b77b6818' , 'tom' : '1702a132e769a623c1adb78353fc9503' } import hashlib assert hashlib.md5(passwd.encode()).hexdigest() == db[user]
现在的执行结果为:
1 2 3 4 5 6 7 8 9 10 11 $ pytest -v tests/test-function/test_parametrize.py::test_passwd_md5_id ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- c:\anaconda3\python.exe cachedir: .pytest_cache rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests/test-function/test_parametrize.py::test_passwd_md5_id[User<Jack>] PASSED [ 50%] tests/test-function/test_parametrize.py::test_passwd_md5_id[User<Tom>] PASSED [100%] ========================== 2 passed in 0.07 seconds ===========================
3、夹具 3.1 什么是夹具 夹具(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。
我们可以利用夹具做任何事情,其中最常见的可能就是数据库的初始连接和最后关闭操作。
Pytest 使用 pytest.fixture()
定义夹具,下面是最简单的夹具,只返回北京邮编:
1 2 3 4 5 6 7 8 9 @pytest.fixture() def postcode (): return '010' def test_postcode (postcode ): assert postcode == '010'
夹具可以直接定义在各测试脚本中,就像上面的例子。更多时候,我们希望一个夹具可以在更大程度上复用,这就需要对夹具进行集中管理。Pytest 使用文件 conftest.py
集中管理夹具。
在复杂的项目中,可以在不同的目录层级定义 conftest.py
,其scope为其所在的目录和子目录。
*不要自己显式调用 conftest.py
,pytest 会自动调用,可以把 conftest 当做插件来理解。*
3.2 setup和teardown 很多时候需要在测试前进行预处理(如新建数据库连接),并在测试完成进行清理(关闭数据库连接)。
当有大量重复的这类操作,最佳实践是使用夹具来自动化所有预处理和后处理。
Pytest 使用 yield
关键词将夹具分为两部分,yield
之前的代码属于预处理,会在测试前执行;yield
之后的代码属于后处理,将在测试完成后执行。
以下测试模拟数据库查询,使用夹具来模拟数据库的连接关闭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @pytest.fixture() def db (): print ('Connection successful' ) yield print ('Connection closed' ) def search_user (user_id ): d = { '001' : 'xiaoming' } return d[user_id] def test_search (db ): assert search_user('001' ) == 'xiaoming'
执行时使用 -s
阻止消息被吞:
1 2 3 4 5 6 7 8 9 10 11 $ pytest -s tests/fixture/test_db.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\fixture\test_db.py Connection successful .Connection closed ========================== 1 passed in 0.02 seconds ===========================
可以看到在测试成功的 .
标识前后有数据库的连接和关闭操作。
如果想更细的跟夹具执行,可以使用 --setup-show
选项:
1 2 3 4 5 6 7 8 9 10 11 12 $ pytest --setup-show tests/fixture/test_db.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\fixture\test_db.py SETUP F db tests/fixture/test_db.py::test_search (fixtures used: db). TEARDOWN F db ========================== 1 passed in 0.03 seconds ===========================
3.3 scope使用 夹具的作用是为了抽离出重复的工作和方便复用,为了更精细化控制夹具(比如只想对数据库访问测试脚本使用自动连接关闭夹具),pytest 使用scope来进行指夹具的使用范围。
在定义夹具时,通过 scope
参数声明scope,可选项有:
function
: 函数级,每个测试函数都会执行一次夹具;
class
: 类级别,每个测试类执行一次,所有方法都可以使用;
module
: 模块级,每个模块执行一次,模块内函数和方法都可使用;
session
: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
默认的scope为 function
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @pytest.fixture(scope='function' ) def func_scope (): pass @pytest.fixture(scope='module' ) def mod_scope (): pass @pytest.fixture(scope='session' ) def sess_scope (): pass @pytest.fixture(scope='class' ) def class_scope (): pass
最简单使用夹具方式是作为测试函数参数:
1 2 3 4 def test_multi_scope (sess_scope, mod_scope, func_scope ): pass
执行结果如下,可以清楚看到各夹具的scope和执行顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ pytest --setup-show tests/fixture/test_scope.py::test_multi_scope ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\fixture\test_scope.py SETUP S sess_scope SETUP M mod_scope SETUP F func_scope tests/fixture/test_scope.py::test_multi_scope (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope TEARDOWN M mod_scope TEARDOWN S sess_scope ========================== 1 passed in 0.10 seconds ===========================
对于类使用scope,需要使用 pytest.mark.usefixtures
(对函数和方法也适用):
1 2 3 4 5 6 7 8 9 @pytest.mark.usefixtures('class_scope' ) class TestClassScope : def test_1 (self ): pass def test_2 (self ): pass
执行结果如下,可见所有测试函数都在夹具的scope内:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ pytest --setup-show tests/fixture/test_scope.py::TestClassScope ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests\fixture\test_scope.py SETUP C class_scope tests/fixture/test_scope.py::TestClassScope::()::test_1 (fixtures used: class_scope). tests/fixture/test_scope.py::TestClassScope::()::test_2 (fixtures used: class_scope). TEARDOWN C class_scope ========================== 2 passed in 0.03 seconds ===========================
3.4 自动执行夹具 目前为止,所有夹具的使用都是手动指定,或者作为参数,或者使用 usefixtures
。
如果我们想让夹具自动执行,可以在定义时指定 autouse
参数。
下面是两个自动计时夹具,一个用于统计每个函数运行时间(function
),一个用于计算测试总耗时(session
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 DATE_FORMAT = '%Y-%m-%d %H:%M:%S' @pytest.fixture(scope='session' , autouse=True ) def timer_session_scope (): start = time.time() print ('\nstart: {}' .format (time.strftime(DATE_FORMAT, time.localtime(start)))) yield finished = time.time() print ('finished: {}' .format (time.strftime(DATE_FORMAT, time.localtime(finished)))) print ('Total time cost: {:.3f}s' .format (finished - start)) @pytest.fixture(autouse=True ) def timer_function_scope (): start = time.time() yield print (' Time cost: {:.3f}s' .format (time.time() - start))
注意下面的两个测试函数并都没有直接使用夹具:
1 2 3 4 5 6 def test_1 (): time.sleep(1 ) def test_2 (): time.sleep(2 )
执行测试可看到夹具自动执行并完成计时任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ pytest -s tests/fixture/test_autouse.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests\fixture\test_autouse.py start: 2018-06-12 10:16:27 . Time cost: 1.003s. . Time cost: 2.003s. finished: 2018-06-12 10:16:30 Total time cost: 3.016s. ========================== 2 passed in 3.11 seconds ===========================
3.5 夹具重命名 夹具的名称默认为定义时的函数名,如果不想使用默认,可以通过 name
选项指定名称:
1 2 3 4 5 6 7 8 9 @pytest.fixture(name='age' ) def calculate_average_age (): return 28 def test_age (age ): assert age == 28
3.6 传递参数 在“第二部分 测试函数”中,介绍了函数的参数化测试:
1 2 3 4 5 6 7 8 @pytest.mark.parametrize('passwd' , ['123456' , 'abcdefdfs' , 'as52345fasdf4' ] )def test_passwd_length (passwd ): assert len (passwd) >= 8
因为夹具也是函数,我们同样可以对夹具进行参数化。在什么情况下需要对夹具参数化?
假设现在有一批 API 需要测试对不同数据库的支持情况(对所有数据库进行相同操作),最简单的方法就是针对每个数据库编写一个测试用例,但这包含大量重复代码,如数据库的连接、关闭,查询等。
进一步,可以使用夹具抽离出数据库的通用操作,每个 API 都能复用这些数据库夹具,同时可维护性也得到提升。
更进一步,可以继续将这些夹具合并为一个,而通过参数控制连接到不同的数据库。这就需要使用夹具参数化来实现。夹具参数化需要使用 pytest 内置的夹具 request
,并通过 request.param
获取参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @pytest.fixture(params=[ ('redis' , '6379' ), ('elasticsearch' , '9200' ) ] )def param (request ): return request.param @pytest.fixture(autouse=True ) def db (param ): print ('\nSucceed to connect %s:%s' % param) yield print ('\nSucceed to close %s:%s' % param) def test_api (): assert 1 == 1
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ pytest -s tests/fixture/test_parametrize.py ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 2 items tests\fixture\test_parametrize.py Succeed to connect redis:6379 . Succeed to close redis:6379 Succeed to connect elasticsearch:9200 . Succeed to close elasticsearch:9200 ========================== 2 passed in 0.10 seconds ===========================
与函数参数化使用 @pytest.mark.parametrize
不同,夹具在定义时使用 params
参数进行参数化。
夹具参数化依赖于内置夹具 request
及其属性 param
。
3.7 内置夹具 3.7.1 tmpdir & tmpdir_factory 用于临时文件和目录管理,默认会在测试结束时删除。
tmpdir
只有 function
scope,只能在函数内使用。
使用 tmpdir.mkdir()
创建目临时录,tmpdir.join()
创建临时文件(或者使用创建的目录)。
1 2 3 4 5 6 7 def test_tmpdir (tmpdir ): a_dir = tmpdir.mkdir('mytmpdir' ) a_file = a_dir.join('tmpfile.txt' ) a_file.write('hello, pytest!' ) assert a_file.read() == 'hello, pytest!'
tmpdir_factory
可以在所有scope使用,包括 function, class, module, session
。
1 2 3 4 5 6 7 8 @pytest.fixture(scope='module' ) def my_tmpdir_factory (tmpdir_factory ): a_dir = tmpdir_factory.mktemp('mytmpdir' ) a_file = a_dir.join('tmpfile.txt' ) a_file.write('hello, pytest!' ) return a_file
3.7.2 pytestconfig 使用 pytestconfig
,可以很方便的读取命令行参数和配置文件。
下面示例演示命令行参数解析:首先在 conftest.py
中使用函数 pytest_addoption
( hook function ):
1 2 3 4 5 def pytest_addoption (parser ): parser.addoption('--host' , action='store' , help ='host of db' ) parser.addoption('--port' , action='store' , default='8888' , help ='port of db' )
然后就可以在测试函数中通过 pytestconfig
获取命令行参数:
1 2 3 4 5 def test_option1 (pytestconfig ): print ('host: %s' % pytestconfig.getoption('host' )) print ('port: %s' % pytestconfig.getoption('port' ))
pytestconfig
其实是 request.config
的快捷方式,所以也可以自定义夹具实现命令行参数读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def pytest_addoption (parser ): parser.addoption('--host' , action='store' , help ='host of db' ) parser.addoption('--port' , action='store' , default='8888' , help ='port of db' ) @pytest.fixture def config (request ): return request.config def test_option2 (config ): print ('host: %s' % config.getoption('host' )) print ('port: %s' % config.getoption('port' ))
执行结果:
1 2 3 4 5 6 7 8 9 10 11 $ pytest -s --host=localhost tests/fixture/test_config.py::test_option2 ============================= test session starts ============================= platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 rootdir: F:\self-repo\learning-pytest, inifile: collected 1 item tests\fixture\test_config.py host: localhost port: 8888 . ========================== 1 passed in 0.06 seconds ===========================
3.7.3 capsys capsys
用于捕获 stdout
和 stderr
的内容,并临时关闭系统输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def ping (output ): print ('Pong...' , file=output) def test_stdout (capsys ): ping(sys.stdout) out, err = capsys.readouterr() assert out == 'Pong...\n' assert err == '' def test_stderr (capsys ): ping(sys.stderr) out, err = capsys.readouterr() assert out == '' assert err == 'Pong...\n'
3.7.4 monkeypatch monkeypath
用于运行时动态修改类或模块。
在 Python 中,“monkeypatch ”仅指在运行时对类或模块进行的动态修改,来修补现有的第三方代码,以解决与期望不符的 bug 或功能。
一个简单的 monkeypatch 如:
1 2 3 4 5 6 7 from SomeOtherProduct.SomeModule import SomeClassdef new_speak (self ): return "ook ook eee eee eee!" SomeClass.speak = new_speak
Pytest 内置 monkeypatch
提供的函数有:
setattr(target, name, value, raising=True)
,设置属性;
delattr(target, name, raising=True)
,删除属性;
setitem(dic, name, value)
,字典添加元素;
delitem(dic, name, raising=True)
,字典删除元素;
setenv(name, value, prepend=None)
,设置环境变量;
delenv(name, raising=True)
,删除环境变量;
syspath_prepend(path)
,添加系统路径;
chdir(path)
,切换目录。
其中 raising
用于通知 pytest 在元素不存在时是否抛出异常;prepend
如果设置,环境变量将变为 value+prepend+
。
下面使用保存配置文件示例说明 monkeypatch 的作用和使用。
假设我们需要切换某个服务到国内科大源以加速,有以下脚本用于修改配置文件 .conf.json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 def dump_config (config ): path = os.path.expanduser('~/.conf.json' ) with open (path, 'w' , encoding='utf-8' ) as wr: json.dump(config, wr, indent=4 ) def test_config (): dump_config(config) path = os.path.expanduser('~/.conf.json' ) expected = json.load(open (path, 'r' , encoding='utf-8' )) assert expected == config
似乎测试正常执行完全没有问题,但如果我们的家目录下恰好有这个配置文件并且维护了许多配置,运行测试将会覆盖原有配置,这太可怕了!
所以我们需要修改测试,最好能在临时目录里完成。但程序已经写死了文件路径,怎么办?
这种在运行时控制程序的功能就需要 monkeypatch 来实现,下面在测试过程中修改了环境变量:
1 2 3 4 5 6 7 8 9 def test_config_monkeypatch (tmpdir, monkeypatch ): monkeypatch.setenv('HOME' , tmpdir.mkdir('home' )) dump_config(config) path = os.path.expanduser('~/.conf.json' ) expected = json.load(open (path, 'r' , encoding='utf-8' )) assert expected == config
现在测试会来临时目录中执行,但环境变量可能对系统有依赖,所以更好的解决方法能自己控制路径中 ~
的替换,这次通过改变 os.path.expanduser
的行为来实现:
1 2 3 4 5 6 7 8 9 10 def test_config_monkeypatch2 (tmpdir, monkeypatch ): fake_home = tmpdir.mkdir('home' ) monkeypatch.setattr (os.path, 'expanduser' , lambda x: x.replace('~' , str (fake_home))) dump_config(config) path = os.path.expanduser('~/.conf.json' ) expected = json.load(open (path, 'r' , encoding='utf-8' )) assert expected == config
3.7.5 recwarn recwarn
用于捕获程序中 warnings
产生的警告。
1 2 3 4 5 6 7 8 9 10 11 def warn (): warnings.warn('Deprecated function' , DeprecationWarning) def test_warn (recwarn ): warn() assert len (recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning
此外,pytest 可以使用 pytest.warns()
捕获警告:
1 2 3 4 5 6 7 def test_warn2 (): with pytest.warns(None ) as warnings: warn() assert len (warnings) == 1 w = warnings.pop() assert w.category == DeprecationWarning
4、钩子函数 钩子函数允许在测试执行过程中的特定阶段注入逻辑,根据测试时间修改或者扩展测试的行为。通常钩子函数写在conftest.py
文件里面
4.1 界定自定义装饰符 自定义pytest_configure
接收一个参数config
和一个pytest.config
对象,来代表配置信息。他不会返回任何内容,但是可以使用config
对象来访问和修改Pytest配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def pytest_configure (config ): config.addinivalue_line("markers" , "ui: mark test as a UI test" ) config.addinivalue_line("markers" , "api: mark test as an API test" ) import pytest@pytest.mark.ui def test_ui_component (): print ("Testing UI component" ) @pytest.mark.api def test_api_call (): print ("Testing API call" )
在上述代码中test_ui_component
和test_api_call
分别被装饰为ui
和api
。
如果想仅运行ui
测试,可以通过-m标识符并指定标识名称。
4.2 共享全局变量 通过pytest_configure可以定义在测试中共享的全局变量。
1 2 3 def pytest_configure (config ): config.my_global_data = "Shared Value"
上述代码定义了一个值为Shared Value
的全局变量my_global_data
,这个全局变量可以使用request
夹具通过config
对象访问。
1 2 3 4 5 6 7 8 import pytestdef test_example (request ): global_value = request.config.my_global_variable print (f"The global variable is: {global_value} " )
4.3 访问命令行选项 我们可以通过pytest_addoption
来访问命令行输入的参数。
1 2 3 4 5 6 def pytest_addoption (parser ): parser.addoption("--custom-option" , action="store" , default="default" ) def pytest_configure (config ): config.custom_option = config.getoption("--custom-option" )
parser.addoption
定义了一个新的命令行选项。在pytest_configure
中,我们传递了命令行参数--custom-option myValue
。总结来说,parser.addoption
用来从命令行获取参数,pytest_configure
用来将命令行中获取参数变成全局变量。
1 2 3 4 5 6 7 8 9 10 parser_addoption(name,action,default,help ) import pytestdef test_custom_option (pytestconfig ): assert pytestconfig.custom_option == "myValue"
上述代码使用pytestconfig
可以直接访问全局变量
命令行输入:
1 pytest --custom-option=myValue