本文将会介绍Python最常用的测试框架之一的pytest,介绍该测试框架的常见用法,也是笔者近期使用该框架的一些总结。
pytest是一个功能强大的Python测试框架,广泛用于编写和运行单元测试、集成测试和功能测试。它与Python自带的unittest框架相似,但在使用上更为简洁和高效。
pytest的主要特点如下:
易于上手:pytest具有直观的API,用户可以快速编写测试用例。
支持多种测试类型:能够处理简单的单元测试到复杂的功能测试,包括Web自动化测试(如Selenium)和接口自动化测试(如requests)。
参数化功能:允许通过参数化来简化多个用例的编写,使得同一测试函数可以使用不同的输入进行多次验证。
丰富的插件生态:pytest支持多种第三方插件,如pytest-html(生成HTML格式的测试报告)、pytest-xdist(支持并行执行测试)等,用户还可以自定义扩展。
灵活的跳过和失败处理:在测试执行过程中,可以选择跳过某些测试或标记预期失败的用例。
与持续集成工具兼容:pytest能够方便地与Jenkins等持续集成工具进行集成。
pytest可以通过pip轻松安装。打开命令行并输入以下命令:
安装完成后,可以通过以下命令检查版本以确认安装成功:
快速入门
一个简单的例子
以下是一个测试脚本的Python代码,文件名称为test_square.py。
1 2 3 4 5 6 7 8 9 10 11 12 import mathdef test_sqrt (): num = 25 assert math.sqrt(num) == 5 def test_square (): num = 7 assert num ** 2 == 40
使用命令行测试:
输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 collected 2 items test_square.py .F [100%] ======================================================== FAILURES ========================================================= ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ___ test_square __ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ _ def test_square(): num = 7 > assert num ** 2 == 40 E assert (7 ** 2) == 40 test_square.py:11: AssertionError ================================================= short test summary info ================================================= FAILED test_ square.py::test_square - assert (7 ** 2) == 40 =============================================== 1 failed, 1 passed in 0.06s ===============================================
可以看到,上面测试命令运行了2个测试case,其中一个运行成功,用.
标识,另一个运行失败,用F
标识,并给出了报错信息。
如果需要输出详细信息,则在命令行中加入-v参数。
1 pytest -v test_square.py
输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 collected 2 items test_square.py::test_ sqrt PASSED [ 50%] test_square.py::test_ square FAILED [100%] ======================================================== FAILURES =========================================================____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ___ test_square _ __ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ def test_square(): num = 7 > assert num ** 2 == 40 E assert (7 ** 2) == 40 test_square.py:11: AssertionError ================================================= short test summary info ================================================= FAILED test_ square.py::test_square - assert (7 ** 2) == 40 =============================================== 1 failed, 1 passed in 0.06s ===============================================
注意 :测试文件名应以test_开头或以_test结尾。测试函数要以test_开头,不然不会加入测试中。测试类名应以Test开头,并且不能包含__init__方法。
如果将上述函数中的test_square函数名称改为a_test_square,则该函数将不会加入测试,使用pytest命令行运行(加-v参数)时,输出结果为:
1 2 3 4 5 collected 1 item test_square.py::test_sqrt PASSED [100%] ==================================================== 1 passed in 0.03s ====================================================
可以看到,函数名称为a_test_square的测试例子将不会加入测试,上述的测试只运行了一个测试例子,且测试成功。
注意 :
如果在测试文件夹下只运行pytest -v
,则pytest将会运行该目录下的所有测试case。
测试标记(Mark)
标记测试函数
默认情况下,pytest会执行文件中的所有以test_开头或结尾的测试函数。
如果我们需要运行特定的测试函数,则可以通过以下三种方式实现:
显式指定函数名,通过::
标记,比如命令pytest test_square.py::test_sqrt
,则只会运行test_square.py中的test_sqrt函数
使用模糊匹配,使用 -k
选项标识,比如命令pytest -k sqrt test_square.py
使用pytest.mark
在函数上进行标记。
使用pytest.mark
时,对测试函数进行标识,如以下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import mathimport pytest@pytest.mark.finish def test_sqrt (): num = 25 assert math.sqrt(num) == 5 @pytest.mark.progress def test_square (): num = 7 assert num ** 2 == 40
测试时使用-m
选择标记的测试函数,如命令:
1 pytest -v -m finish test_square.py
输出结果为:
1 2 3 collected 2 items / 1 deselected / 1 selected test_square .py::test_sqrt PASSED [100%]
注意 :在使用mark功能时,同一个测试函数可以打多个标识,多个函数也可以打相同的标识。
跳过测试
使用pytest.mark.skip
装饰器可跳过指定的测试函数。如以下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 import mathimport pytestdef test_sqrt (): num = 25 assert math.sqrt(num) == 5 @pytest.mark.skip(reason="Not implemented yet" ) def test_square (): num = 7 assert num ** 2 == 40
使用命令行pytest test_square.py
,输出结果如下:
1 2 3 4 5 6 collected 2 items test_square.py::test_sqrt PASSED [ 50%] test_square.py::test_square SKIPPED (unconditional skip) [100%] ============================================== 1 passed, 1 skipped in 0.03s ===============================================
可以看到,第二个测试函数被跳过了,不会加入此次测试。
pytest还支持使用 pytest.mark.skipif 为测试函数指定被忽略的条件。
标识预见错误
使用pytest.mark.xfail
实现预见错误功能,即该测试函数的错误已经被预知了,因此测试结果为通过。如以下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 import mathimport pytestdef test_sqrt (): num = 25 assert math.sqrt(num) == 5 @pytest.mark.xfail(reason="I know this will fail" ) def test_square (): num = 7 assert num ** 2 == 40
使用命令pytest -v test_square.py
进行测试,输出结果如下:
1 2 3 4 5 6 collected 2 items test_square.py::test_sqrt PASSED [ 50 test_square.py::test_square XFAIL (I know this will fail ) [100 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 1 passed, 1 xfailed in 0.04 s = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
可以看到test_square函数的测试结果为XFAIL,该测试也是通过的。
参数化测试
如果我们的测试函数需要对参数的多个值进行测试,一种方法是直接在测试函数中测试多个值的情形,但这只是一个测试case。较好的解决方法是参数化测试,即每组参数都独立执行一次测试。如以下的脚本:
1 2 3 4 5 6 import pytest@pytest.mark.parametrize("num" , [1 , 2 , 3 , 4 ] ) def test_square (num: int ): assert 0 < num ** 2 < 10
执行测试,输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 collected 4 items test_square.py::test_ square[1] PASSED [ 25%] test_square.py::test_ square[2] PASSED [ 50%] test_square.py::test_ square[3] PASSED [ 75%] test_square.py::test_ square[4] FAILED [100%] ======================================================== FAILURES =========================================================____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ _ test_square[4] ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ _ _ num = 4 @pytest.mark.parametrize("num", [1, 2, 3, 4]) def test_square(num: int): > assert 0 < num ** 2 < 10 E assert (4 ** 2) < 10 test_square.py:6: AssertionError ================================================= short test summary info ================================================= FAILED test_ square.py::test_square[4] - assert (4 ** 2) < 10 =============================================== 1 failed, 3 passed in 0.07s ===============================================
可以看到,上面的参数化测试执行了4个测试case,3个成功,1个失败,这使得参数化测试很有优势。
如果我们需要将测试组的默认参数显示更清晰,我们可以使用pytest.param
的id
参数进行自定义,修改如下:
1 2 3 4 5 6 7 8 9 10 import pytest@pytest.mark.parametrize("num" , [pytest.param(1 , id ='case_1' ), pytest.param(2 , id ='case_2' ), pytest.param(3 , id ='case_3' ), pytest.param(4 , id ='case_4' )] )def test_square (num: int ): assert 0 < num ** 2 < 10
如果需要将测试函数中的参数调整为多个参数,那么可以修改如下:
1 2 3 4 5 6 import pytest@pytest.mark.parametrize("x, y" , [(1 , 2 ), (3 , 4 )] ) def test_square (x: int , y: int ): assert 0 < x ** 2 + y ** 2 < 10
测试固件(Fixture)
固件(Fixture)是一些函数,pytest
会在执行测试函数之前(或之后)加载运行它们,类似于unittest
框架
的相关测试固件,如setup
、teardown
等。
pytest使用pytest.fixture()
来定义固件,更多时候,我们会使用conftest.py
文件来管理固件。
调用方法
固件的调用方式有很多,这里介绍三种常见的调用方法:
如下面的脚本所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import pytest@pytest.fixture def get_data (): data = {'name' : 'John' , 'age' : 25 } return datadef test_user_name (get_data ): assert get_data['name' ] == 'John' def test_user_age (get_data ): assert get_data['age' ] == 25
在上述例子中,test_user_name和test_user_age测试函数都是使用get_data固件。
使用@pytest.mark.usefixtures('fixture函数名')
装饰器
此时上面的脚本修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import pytest data = {}@pytest.fixture def get_data (): global data data = {'name' : 'John' , 'age' : 25 }@pytest.mark.usefixtures('get_data' ) def test_user_name (): assert data['name' ] == 'John' @pytest.mark.usefixtures('get_data' ) def test_user_age (): assert data['age' ] == 25
需要注意的是,这里将data设置为全局变量,仅作为本次例子演示使用。
此时,上述的脚本修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import pytest data = {}@pytest.fixture(autouse=True ) def get_data (): global data data = {'name' : 'John' , 'age' : 25 }def test_user_name (): assert data['name' ] == 'John' def test_user_age (): assert data['age' ] == 25
当然,
pytest也支持调用多个固件,和对固件进行参数化测试。以下是一个结合多个固件和参数化测试的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import pytest data = {}@pytest.fixture(params=[{'name' : 'John' , 'age' : 25 }, {'name' : 'Tom' , 'age' : 30 }] )def get_data (request ): global data data = request.param@pytest.fixture() def set_user_country (): global data data['country' ] = 'US' @pytest.mark.usefixtures('get_data' ) @pytest.mark.usefixtures('set_user_country' ) def test_user_name_and_age (): assert data['name' ] in ['John' , 'Alan' ] assert 0 < data['age' ] < 100 @pytest.mark.usefixtures('get_data' ) @pytest.mark.usefixtures('set_user_country' ) def test_user_country (): assert data['country' ] == 'US'
上面的例子中,共有2个固件:get_data,
set_user_country,其中get_data设置data中的name和age,采用参数化测试,而set_user_country则设置data中的country.
执行上述测试,结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 collected 4 items test_square.py::test_user_name_and_age[get_data0] PASSED [ 25 test_square.py::test_user_name_and_age[get_data1] FAILED [ 50 test_square.py::test_user_country[get_data0] PASSED [ 75 test_square.py::test_user_country[get_data1] PASSED [100 ======================================================== FAILURES =========================================================____________________________________________ test_user_name_and_age[get_data1] ____________________________________________ @pytest.mark.usefixtures('get_data' ) @pytest.mark.usefixtures('set_user_country' ) def test_user_name_and_age(): > assert data['name' ] in ['John' , 'Alan' ]E AssertionError : assert 'Tom' in ['John' , 'Alan' ] test_square.py:23 : AssertionError ================================================= short test summary info =================================================FAILED test_square.py::test_user_name_and_age[get_data1] - AssertionError : assert 'Tom' in ['John' , 'Alan' ] =============================================== 1 failed, 3 passed in 0.06 s ===============================================
与我们预期的一样,共有4个测试case,其中test_user_name_and_age当参数为{'name':
'Tom', 'age': 30}时会报错。
作用域
pytest的固件的作用域(scope)分为session
、module
、class
、function
四个级别。在定义fixture函数的时候通过scope参数指定作用范围,默认为function。
function :
函数级,每个测试函数都会执行一次固件;
class :
类级别,每个测试类执行一次,所有方法都可以使用;
module :
模块级,每个模块执行一次,模块内函数和方法都可使用;
session :
会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
关于固件的作用域,将在下面的配置文件conftest.py
中进行演示。
配置文件conftest.py
在pytest框架中,conftest.py文件的主要用途是定义和共享测试固件(fixtures)及其他配置选项,以便在多个测试文件之间重用。这种方式可以减少代码重复,提高测试的可维护性和可读性。
conftest.py的用途如下:
定义共享固件:
固件是pytest中的一种功能,用于设置测试环境或者提供测试数据。通过在conftest.py中定义固件,可以使其在同一目录下的所有测试文件中可用。
配置命令行选项:
可以在conftest.py中添加自定义的命令行选项,以便在运行pytest时传递参数。
添加钩子函数:
可以使用钩子函数来扩展pytest的功能,例如修改测试收集过程或报告输出。
简单例子
以下是一个简单的示例,展示了如何在conftest.py中定义一个夹具,并在不同的测试文件中使用它:
创建 conftest.py
1 2 3 4 5 6 import pytest@pytest.fixture def input_value (): return 42
创建测试文件 test_example1.py
1 2 3 4 5 6 7 8 import pytestdef test_divisible_by_2 (input_value ): assert input_value % 2 == 0 def test_divisible_by_3 (input_value ): assert input_value % 3 == 0
创建测试文件 test_example2.py
1 2 3 4 5 6 7 8 import pytestdef test_divisible_by_6 (input_value ): assert input_value % 6 == 0 def test_divisible_by_7 (input_value ): assert input_value % 7 == 0
运行测试
在终端中运行以下命令来执行这些测试:
1 pytest -v test_example1.py test_example2.py
输出示例如下所示:
1 2 3 4 test_example1.py::test_divisible_by_2 PASSED test_example1.py::test_divisible_by_3 FAILED test_example2.py::test_divisible_by_6 PASSED test_example2.py::test_divisible_by_7 FAILED
通过这个示例,可以看到input_value夹具被成功地在多个测试文件中重用,从而避免了在每个测试文件中重复定义相同的数据或设置。这种方法使得测试代码更加整洁和易于管理。
作用域演示
conftest.py如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import pytest@pytest.fixture(scope="session" ) def session_fixture (): print ("这是一个作用于session的fixture" )@pytest.fixture(scope="module" ) def module_fixture (): print ("这是一个作用于module的fixture" )@pytest.fixture(scope="class" ) def class_fixture (): print ("这是一个作用于class的fixture" )@pytest.fixture(scope="function" ) def function_fixture (): print ("这是一个作用于function的fixture" )
在test_example1.py测试文件中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pytest@pytest.mark.usefixtures("class_fixture" ) @pytest.mark.usefixtures("function_fixture" ) class TestOrder : def test_a (self ): print ("test_a" ) def test_b (self ): print ("test_b" )@pytest.mark.usefixtures("function_fixture" ) def test_c (): print ("test_c" )
在文件中,TestOrder类使用了class_fixture,
function_fixture固件,test_c只使用了function_fixture固件。
运行命令pytest -v -s test_example1.py
后(-s支持终端输出),结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 collected 3 items test_example1.py ::TestOrder::test_a 这是一个作用于class的fixture 这是一个作用于function 的fixture test_a PASSED test_example1.py ::TestOrder::test_b 这是一个作用于function 的fixture test_b PASSED test_example1.py ::test_c 这是一个作用于function 的fixture test_c PASSED ==================================================== 3 passed in 0.02 s ====================================================
TestOrder类中,class_fixture执行了一次,而test_a,
test_b分别执行function_fixture一次。
异步测试
在pytest中进行异步测试,通常会用到 pytest-asyncio
插件,可直接使用pip方式安装。这个插件为pytest提供了对asyncio协程的支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import pytestimport asyncioasync def async_function (x ): await asyncio.sleep(1 ) return x * 2 @pytest.mark.asyncio async def test_async_function (): result = await async_function(3 ) assert result == 6
运行测试命令如下:
总结
本文主要介绍了Python的主流测试框架之一的pytest,分别展示了pytest的基本用法,标识,固件,异步测试等使用方法,算是对pytest有了一个初步的了解。
后续文章将会介绍pytest的高级用法,敬请期待~