文章目录
1. 前言
pytest 是一个非常强大的python测试框架,熟练掌握pytest对于自动化测试工程非常重要,下面来分享一些我的使用经验
2. pytest 安装
安装命令, 要求:Python 3.8+ or PyPy3.
pip install -U pytest
安装完成可以可以通过--version
来检验安装是否成功
$ pytest --version
pytest 8.3.3
3. 快速开始
pytest测试文件必须要以test_*.py
或者*_test.py
格式命名的文件,pytest会检查文件中所有的以test_*
开头的方法,或者以Test*
开头的类,通过断言来判断case是否成功。下面我们来一个简单的例子
# content of test_sample.py
def func(x):
return x + 1
def test_answer():
assert func(3) == 5
运行测试
pytest test_sample.py
输出结果:
============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 1 item
test_sample.py::test_answer FAILED [100%]
================================================================== FAILURES ==================================================================
________________________________________________________________ test_answer _________________________________________________________________
def test_answer():
> assert func(3) == 5
E assert 4 == 5
E + where 4 = func(3)
test_sample.py:6: AssertionError
========================================================== short test summary info ===========================================================
FAILED test_sample.py::test_answer - assert 4 == 5
============================================================= 1 failed in 0.03s ==============================================================
通过结果的汇报我们可以很容易找到test_sample.py::test_answer
测试失败
上面展示了一个最简单的例子,在现实工程中往往不止一个case,当有多个case时,pytest可以通过::
来指定运行规则
- 运行一个指定的case
pytest tests/test_mod.py::test_func
- 运行一个测试类中所以的case
pytest tests/test_mod.py::TestClass
- 运行一个测试类中指定case
pytest tests/test_mod.py::TestClass::test_method
- 运行一个特定参数的case
pytest tests/test_mod.py::test_func[x1,y2]
pytest运行时有一些常用参数,熟练掌握这些参数会让UT的调试变得非常顺利
- -k: 会运行匹配一个关键字符的测试集,比如
pytest -k hello
,会打印所有带有hello的测试用例 - -v: 打印测试进度
- -s: 终端打印输入,当跟-v一起使用时,就可以缩写成
-sv
- -m: 运行指定标记的测试集,比如
pytest -m sanity
,表示运行sanity
测试集 - –junitxml: 设置输出xml结果的文件
4. 使用fixtures
使用pytest, fixture就是pytest的精髓所在,熟练掌握fixture,可以做很多setup和teardown的工作
4.1 fixture用途
- 做测试前后的初始化设置,如测试数据准备,链接数据库,打开浏览器等这些操作都可以使用fixture来实现
- 测试用例的前置条件可以使用fixture实现
- 与yield关键字可以实现teardown功能
4.2 fixture实战
fixture通过@pytest.fixture()装饰器装饰一个函数,那么这个函数就是一个fixture,目前常用的使用方法有三种
- 被@pytest.fixture()装饰的函数被作为函数参数使用
# test_fixture.py
import pytest
@pytest.fixture()
def fixtureFunc():
return 'fixtureFunc'
def test_fixture(fixtureFunc):
print('我调用了{}'.format(fixtureFunc))
if __name__=='__main__':
pytest.main(['-v', 'test_fixture.py'])
执行结果
test_fixture.py .我调用了fixtureFunc
[100%]
========================== 1 passed in 0.02 seconds ===========================
Process finished with exit code 0
fixtureFunc 这个函数就是一个fixture,fixture函数内部可以实现一些初始化操作, 它被函数test_fixture作为参数进行调用, 测试中所需要的参数,会通过返回值的形式被调用
2. 使用@pytest.mark.usefixtures(‘fixture’)装饰器
# test_fixture.py
import pytest
@pytest.fixture()
def fixtureFunc():
print('\n fixture->fixtureFunc')
@pytest.mark.usefixtures('fixtureFunc')
def test_fixture():
print('in test_fixture')
@pytest.mark.usefixtures('fixtureFunc')
class TestFixture(object):
def test_fixture_class(self):
print('in class with text_fixture_class')
if __name__=='__main__':
pytest.main(['-v', 'test_fixture.py'])
fixtureFunc被测试类TestFixture使用@pytest.mark.usefixtures(‘fixtureFunc’)进行调用
3. 设置autouse
参数,实现自动调用
当设置fixture 参数autouse=True
时,测试集中每个测试用例在调用时,都会先调用fixture
# test_fixture.py
import pytest
@pytest.fixture(autouse=True)
def fixtureFunc():
print('\n fixture->fixtureFunc')
def test_fixture():
print('in test_fixture')
class TestFixture(object):
def test_fixture_class(self):
print('in class with text_fixture_class')
if __name__=='__main__':
pytest.main(['-v', 'test_fixture.py'])
运行结果
============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 2 items
test_sample.py::test_fixture
fixture->fixtureFunc
in test_fixture
PASSED
test_sample.py::TestFixture::test_fixture_class
fixture->fixtureFunc
in class with text_fixture_class
PASSED
============================================================= 2 passed in 0.01s ==============================================================
4.3 fixture作用范围
fixture还可以通过scope参数来指定作用范围。
scope参数可以支持session,package, module,class,function五种作用范围; 默认为function
- function: 默认范围,每个测试执行前都会调用,上面的例子都是这种级别的fixture
- class:类级别,每个类执行前会被执行
- module: 模块级别,模块里所有的用例执行前执行一次模块级的fixture
- package: 包级别,当多个包时,每个包内所有用例执行前执行一次fixture
- session: 会话级别,整个pytest 会话之前执行一次
下面用一个简单的例子来展示一下:
# test_fixture.py
import pytest
@pytest.fixture(scope='module', autouse=True)
def module_fixture():
print('\n-----------------')
print('我是module fixture')
print('-----------------')
@pytest.fixture(scope='class')
def class_fixture():
print('\n-----------------')
print('我是class fixture')
print('-------------------')
@pytest.fixture(scope='function', autouse=True)
def func_fixture():
print('\n-----------------')
print('我是function fixture')
print('-------------------')
def test_1():
print('\n 我是test1')
@pytest.mark.usefixtures('class_fixture')
class TestFixture1(object):
def test_2(self):
print('\n我是class1里面的test2')
def test_3(self):
print('\n我是class1里面的test3')
@pytest.mark.usefixtures('class_fixture')
class TestFixture2(object):
def test_4(self):
print('\n我是class2里面的test4')
def test_5(self):
print('\n我是class2里面的test5')
if __name__=='__main__':
pytest.main(['-v', '--setup-show', 'test_fixture.py'])
运行结果
============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa
configfile: pytest.ini
collected 5 items
test_sample.py::test_1
-----------------
我是module fixture
-----------------
-----------------
我是function fixture
-------------------
我是test1
PASSED
test_sample.py::TestFixture1::test_2
-----------------
我是class fixture
-------------------
-----------------
我是function fixture
-------------------
我是class1里面的test2
PASSED
test_sample.py::TestFixture1::test_3
-----------------
我是function fixture
-------------------
我是class1里面的test3
PASSED
test_sample.py::TestFixture2::test_4
-----------------
我是class fixture
-------------------
-----------------
我是function fixture
-------------------
我是class2里面的test4
PASSED
test_sample.py::TestFixture2::test_5
-----------------
我是function fixture
-------------------
我是class2里面的test5
PASSED
============================================================= 5 passed in 0.02s ==============================================================
4.4 fixture实现teardown
如果要实现teardown,我们可以与yield一起使用,我们都指定yield有类似return的功能,与之不同的是,当调用yield并不会停止执行,而是当所在函数被调出时,会继续执行,于是我们用下面一个简单的例子来展示一下:
import pytest
from selenium import webdriver
import time
@pytest.fixture()
def fixtureFunc():
'''实现浏览器的打开和关闭'''
driver = webdriver.Firefox()
yield driver
driver.quit()
def test_search(fixtureFunc):
'''访问百度首页,搜索pytest字符串是否在页面源码中'''
driver = fixtureFunc
driver.get('http://www.baidu.com')
driver.find_element_by_id('kw').send_keys('pytest')
driver.find_element_by_id('su').click()
time.sleep(3)
source = driver.page_source
assert 'pytest' in source
if __name__=='__main__':
pytest.main(['--setup-show', 'test_fixture.py'])
这个实例会先打开浏览器,然后执行测试用例,最后关闭浏览器。大家可以试试! 通过yield就实现了 用例执行后的teardown功能。进一步的我们用使用module或者session 的作用域来实现整个测试的setup和teardown(爱动手的老铁相信已经开始行动了)。
5. 设置mark属性
pytest支持使用pytest.mark
来轻松设置一些测试函数的metadata,使用户可以使用pytest --markers
来打印所有mark属性以及描述。pytest提供了一些通过的mark属性
- usefixtures: 前面我们用例中已经介绍过,被标记的函数会使用相应的testure
- filterwarnings: 过滤测试函数的所有警告信息
- skip: 跳过当前测试用例
- skipif: 如果条件为true则跳过测试用例
- xfail: 如果特定条件发生,会发生一个failure的异常
- parametrize: 参数化,常用于参数测试
以上的mark属性可能不能满足用户需求,如果想使用自己的mark属性,则需要通过pytest.ini
进行注册,例如:
# pytest.ini
[pytest]
markers =
ci_mini: Run ci_mini case
sanity: Run pr case
regression: Run nightly case
上面注册了ci_mini、sanity、regression等三种属性,平时可以用来测试三种等级的测试,ci_mini
最基础的测试,用于一些流程的打通测试,sanity
可以用于MR的门禁测试, regression
则可以用于大规模压测。下面使用一个例子来展示一下用法
import pytest
@pytest.mark.skip
@pytest.mark.sanity
def test_fun1():
print('in test_fun1')
return False
@pytest.mark.ci_mini
@pytest.mark.sanity
def test_fun2():
print('in test_fun2')
return True
@pytest.mark.sanity
def test_fun3():
print('in test_fun3')
return True
@pytest.mark.regression
def test_fun4():
print('in test_fun4')
return True
从用例可以看到mark属性是可以叠加的。我们可以使用pytest -m
来指定要运行mark属性
pytest -m sanity -sv test_sample.py
运行结果
============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa/build
configfile: pytest.ini
collected 4 items / 1 deselected / 3 selected
test_sample.py::test_fun1 SKIPPED (unconditional skip)
test_sample.py::test_fun2 in test_fun2
PASSED
test_sample.py::test_fun3 in test_fun3
PASSED
============================================================== warnings summary ==============================================================
test_sample.py::test_fun2
/home/ubuntu/.local/lib/python3.8/site-packages/_pytest/python.py:163: PytestReturnNotNoneWarning: Expected None, but test_sample.py::test_fun2 returned True, which will be an error in a future version of pytest. Did you mean to use `assert` instead of `return`?
warnings.warn(
test_sample.py::test_fun3
/home/ubuntu/.local/lib/python3.8/site-packages/_pytest/python.py:163: PytestReturnNotNoneWarning: Expected None, but test_sample.py::test_fun3 returned True, which will be an error in a future version of pytest. Did you mean to use `assert` instead of `return`?
warnings.warn(
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================================== 2 passed, 1 skipped, 1 deselected, 2 warnings in 0.02s ===========================================
6. 参数化
上面我们提到,可以通过mark属性pytest.mark.parametrize
来设置测试的参数,需要注意的是传入的参数必须是一个list,list中的每个元素会通过参数传入, 例如
import pytest
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
运行结果
============================================================ test session starts =============================================================
platform linux -- Python 3.8.10, pytest-8.3.3, pluggy-1.5.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/supa/build
configfile: pytest.ini
collected 3 items
test_sample.py::test_eval[3+5-8] PASSED
test_sample.py::test_eval[2+4-6] PASSED
test_sample.py::test_eval[6*9-42] FAILED
================================================================== FAILURES ==================================================================
_____________________________________________________________ test_eval[6*9-42] ______________________________________________________________
test_input = '6*9', expected = 42
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')
test_sample.py:5: AssertionError
========================================================== short test summary info ===========================================================
FAILED test_sample.py::test_eval[6*9-42] - AssertionError: assert 54 == 42
======================================================== 1 failed, 2 passed in 0.03s =========================================================
7. 并行运行
默认情况下,pytest都是按顺序运行测试,但是在真实工程环境中,往往一个测试套会有很多的测试文件,每个文件都会有一堆测试,顺序执行会导致执行效率较低,为了解决这个问题,pytest为我们提供了并行测试的选项。
如果想使用并行测试,需要先安装 pytest-xdist 插件,安装指令如下:
pip install pytest-xdist
安装成功后,我们就可以使用语法pytest -n <num>
来并行运行,这里的num指定是使用的worker数量,比如使用3个work
pytest -n 3
8. XML格式输出测试结果
默认情况下,pytest只会在终端打印测试结果,如果想保存测试结果,只需要使用 --junitxml=
, 例如:
pytest test_sample.py -v --junitxml="result.xml"
表示保存test_sample.py 的测试结果到到result.xml中