pip install -U pytest
def inc(x):return x+1def test_assert():assert inc(4) == 5class TestAnswer():def test_demo(self):assert 1==1
pytest
,会执行当前目录下所有测试文件pytest test_demo.py
,执行这个文件的所有casepytest test_demo.py::test_assert1
,执行文件下某个函数casepytest test_demo.py::TestDemo
,执行这个文件的这个类下的所有casepytest test_demo.py::TestAnswer::test_demo2
,执行类下面某个方法case::
递进-v -s
参数运行# 模块(文件)级别,当前文件(suite)下所有case前后执行一次
def setup_module():print("\n连接资源")def teardown_module():print("\n释放资源...")# 函数级别,每个函数case执行前后
def setup_function():print("\n函数资源准备")def teardown_function():print("\n函数资源释放...")def inc(x):return x+1def test_assert1():assert inc(4) == 5def test_assert2():assert inc(3) == 4class TestAnswer():# 类级别,只在所有类方法前后执行一次def setup_class(self):print("\nsetup class")def teardown_class(self):print("\nteardown class...")def setup_method(self):print("\nclass method setup")def teardown_method(self):print("\nclass method teardown...")def test_demo1(self):assert 1==1def test_demo2(self):assert 2==2
@pytest.mark.xxx
# 模块(文件)级别,当前文件(suite)下所有case前后执行一次
import pytestdef setup_module():print("\n连接资源")def teardown_module():print("\n释放资源...")# 函数级别,每个函数case执行前后
def setup_function():print("\n函数资源准备")def teardown_function():print("\n函数资源释放...")def inc(x):return x+1@pytest.mark.integer
def test_assert1():print("test print with -s param")assert inc(4) == 5@pytest.mark.integer
def test_assert2():assert inc(3) == 4class TestAnswer():# 类级别,只在所有类方法前后执行一次def setup_class(self):print("\nsetup class")def teardown_class(self):print("\nteardown class...")def setup_method(self):print("\nclass method setup")def teardown_method(self):print("\nclass method teardown...")@pytest.mark.chardef test_demo1(self):assert 1==1@pytest.mark.chardef test_demo2(self):assert 2==2
pytest test_demo1.py -vs -m "integer"
pytest .\test_demo1.py -vs -m "not char"
pytest.ini
文件[pytest]
markers = integerchar
import sys
import pytest@pytest.mark.skip
def test_demo1():print("skip this case")assert True@pytest.mark.skip(reason="开发还没写代码")
def test_demo2():assert Falsedef check():return False# 测试代码里不满足某个条件,直接跳过,有点像skipif
def test_demo3():print("test skip")if not check():pytest.skip("unsupported")print("end")@pytest.mark.skipif(sys.platform=="win32", reason="this kind of platform is not supported!")
def test_demo4():assert True
mark.xfail
,如果case执行成功则XPASS,如果失败则标记为XFAIL,主要是提示的作用,表示这里有个bug还没解决,我们后续可以通过 pytest test_demo1.py -vs -m "xfail"
执行这部分@pytest.mark.xfail
def test_fail1():assert Falsexfail = pytest.mark.xfail # 定义装饰器@xfail
def test_fail2():assert Truedef test_fail3():print("start test")pytest.xfail("功能代码未实现,失败") # 直接让case失败在这里,类似skipprint("end")
import pytestsearch_name = ['selenium', 'appium', 'ut', 'pytest']@pytest.mark.parametrize('name', search_name) # 4个case,取决于参数个数
def test_param1(name):assert name in search_name@pytest.mark.parametrize("_input, expected", [('3+5', 9), ('4+4', 8)])
def test_param2(_input, expected):assert eval(_input)+1 == expected
ids
参数指定case名字,默认是你写的case名称,再拼上参数,参数之间用-
连接,@pytest.mark.parametrize('p1', ['1','2','3'])
@pytest.mark.parametrize('p2', ['4','5','6'])
def test_param3(p1, p2):print(p1, "===", p2)
--lf
,即 --last-failed
,只重新运行失败的cass,(为啥不是 --of)--ff
,即 --failed-first
,先执行上次失败的case,再执行其他测试python test_demo1.py
# test_demo1.py
# 模块(文件)级别,当前文件(suite)下所有case前后执行一次
import pytestdef setup_module():print("\n连接资源")def teardown_module():print("\n释放资源...")# 函数级别,每个函数case执行前后
def setup_function():print("\n函数资源准备")def teardown_function():print("\n函数资源释放...")def inc(x):return x+1@pytest.mark.integer
def test_assert1():print("test print with -s param")assert inc(4) == 5@pytest.mark.integer
def test_assert2():assert inc(3) == 4class TestAnswer():# 类级别,只在所有类方法前后执行一次def setup_class(self):print("\nsetup class")def teardown_class(self):print("\nteardown class...")def setup_method(self):print("\nclass method setup")def teardown_method(self):print("\nclass method teardown...")@pytest.mark.chardef test_demo1(self):assert 1==2@pytest.mark.chardef test_demo2(self):assert 2==2# python test_demo1.py
if __name__ == '__main__':# pytest.main() # 执行当前目录下的所有case,不只是这个文件# 传参,指定case# pytest.main(['test_demo1.py::TestAnswer::test_demo2', '-v'])# 指定Markpytest.main(['test_demo1.py', '-v', '-m', 'char'])
try...except...
raises
def test_raises():# 期望是Value异常with pytest.raises(ValueError) as exp:# 这里面就是我们测试功能的代码,比如用户输入非法值,看是不是我们期望的异常raise ZeroDivisionError('Value must gt 18') # 假装抛出异常,这种情况,case就会fail# assert exp.type is ValueError # 这两句没必要写,逻辑冗余# assert exp.value.args[0] == 'Value must gt 18'
test: 127.0.0.1
import pytest
import yamlclass TestDemo:@pytest.mark.parametrize('env', yaml.safe_load(open('./env.yml')))def test_demo1(self, env):if 'test' in env:print("测试环境")print(env) # test, 只能打印出keyelif 'dev' in env:print("开发环境")
-test: 127.0.0.1t2: 10086-t3: 10010
-
容纳一个字典,作为列表的一个元素,对应一个caseclass TestDemo:@pytest.mark.parametrize('env', yaml.safe_load(open('./env.yml')))def test_demo1(self, env):if 'test' in env:print("测试环境")print(env) # {'test': '127.0.0.1', 't2': 10086}print(env['test']) # 127.0.0.1elif 'dev' in env:print("开发环境")def test_yml(self):# [{'test': '127.0.0.1', 't2': 10086}, {'t3': 10010}]print("\n", yaml.safe_load(open('./env.yml')))
pip install openpyxl
,及基本用法import openpyxl# 打开工作簿
book = openpyxl.load_workbook('./test.xlsx')
# 读取工作表
sheet = book.active
# 读取单元格
c1 = sheet['A2'] # Cell 对象
c2 = sheet.cell(column=1, row=3)
# 读取一片
c3 = sheet['A1':'C3']
# 获取单元格的值
print(c1) # roy
print(c3[0][0].value) # Name
with open
打开,Excel可直接改为CSV文件pip install allure-pytest
import allureclass TestSearch:@allure.title("搜索:测试")def test_demo1(self):print("demo1")
pytest test_allure.py --alluredir ./result --clean-alluredir
,pytest --helpallure serve ./result
suite
(或者说module),suite里面可以有多个类,称为 case
(或feature/TestCase),每个case里面又可以包含多个具体的用例,称为 story
,story有时还可进一步分为多个 keyword
import allure@allure.feature("登录模块")
class TestLogin:# 不加说明会有warning@allure.story("登录成功")def test_login_success(self):print("success")@allure.story("登录失败")def test_login_fail(self):print("fail")
pytest .\test_feature.py --allure-features="登录模块" --alluredir=./result --clean- alluredir
只运行这个feature的story--allure-stories
指定跑哪些story@allure.feature("登录模块")
class TestLogin:# 不加说明会有warning@allure.story("登录成功")@allure.title("fail")def test_login_success(self):with allure.step("1. 打开登录界面"):print("login page")print("输入用户名密码...")with allure.step("2. 跳转到首页"):print("首页...")
link/issue/testcase
pytest test_link.py --alluredir ./result --allure-link-pattern=issue:http://www.bug-platform.com/{} --clean-alluredir
TEST_CASE_LINK = 'https://github.com/qameta/allure-integrations/issues/8#issuecomment-268313637'# 链接 + 名称
@allure.link('https://www.youtube.com/watch?v=Su5p2TqZxKU', name='Click me')
def test_with_named_link():pass# 140这个位置一般是bug号,可以接入自己公司的bug平台,命令行要配置:
# pytest directory_with_tests/ --alluredir=/tmp/my_allure_report --allure-link-pattern=issue:http://www.myself-bug-platform.com/issue/{}
@allure.issue('140', 'Pytest-flaky test retries shows like test steps')
def test_with_issue_link():pass# 超链接到上面的link, 看起来和link好像没什么区别
@allure.testcase(TEST_CASE_LINK, 'Test case title')
def test_with_testcase_link():pass
pytest .\test_severity.py --allure-severities=blocker,trivial --alluredir=./result
import alluredef test_with_no_severity_label():pass# Blocker
@allure.severity(allure.severity_level.BLOCKER)
def test_with_blocker_severity_label():assert 1==2@allure.severity(allure.severity_level.TRIVIAL)
def test_with_trivial_severity():assert 2==4@allure.severity(allure.severity_level.NORMAL)
def test_with_normal_severity():pass@allure.severity(allure.severity_level.NORMAL)
class TestClassWithNormalSeverity(object):def test_inside_the_normal_severity_test_class(self):pass@allure.severity(allure.severity_level.CRITICAL)def test_inside_the_normal_severity_test_class_with_overriding_critical_severity(self):pass
class TestLogin:def test_login_success(self):with allure.step("1. 打开登录界面"):print("login page")allure.attach.file("./sisi.jpg", name="wechat", attachment_type=allure.attachment_type.JPG)with allure.step("2. 跳转到首页"):print("首页...")
allure serve
命令得到在线报告,其实测试报告的生成有完整流程allure generate ./result
allure open -h 127.0.0.1 -p 8883 ./allure-report
,或者在IDE直接打开 index.html,但不能在文件夹直接打开(需要服务器解析,不是静态文件)import pytest@pytest.fixture
def login():print("\n登录成功")# 需要登录,传入被fixture的函数即可
def test_card(login):print("加入购物车成功")
@pytest.fixture(scope="function")
def login():print("\n登录成功")def test_card(login):print("加入购物车成功")def test_search(login):print("搜索商品")
@pytest.fixture(scope="module")
def login():print("\n登录成功")def test_card(login):print("加入购物车成功")def test_search(login):print("搜索商品")
@pytest.fixture(scope="class")
def login():print("\n登录成功")def test_card(login):print("加入购物车成功")def test_search(login):print("搜索商品")class TestClass:def test_demo1(self, login):print("class 1")def test_demo2(self, login):print("class 2")
yield
这里直接返回,但还能回来接着执行后续代码@pytest.fixture(scope="class")
def login():print("\n登录成功")yieldprint("\n登出")def test_card(login):print("加入购物车成功")# 登录成功 加入购物车成功 登出
import pytest@pytest.fixture(scope="session")
def login():print("\n登录成功")yieldprint("\n登出")
import pytest# 设置 autouse
@pytest.fixture(scope="function", autouse=True)
def login():print("\n登录成功")yieldprint("\n登出")
# 不需要写 login,也能使用
def test_conf_1():print("测试 conftest")
import pytest@pytest.fixture(scope="session", params=["roy", "allen"])
def parameter(request):print(f"this is {request.param}")yield request.param # 返回参数print("baibai %s"%request.param)
def test_conf_1(parameter):print("\nfixture parameters test")print("参数为:", parameter)def test_conf_2(parameter):print("\n参数化")print("参数为:", parameter)
check_
开头和 test_
开头的测试文件(suite/module),要加 *
;这是个注释,以分号开头,但是Windows下不能有中文
python_files = check_* test_*
Check
和 Test
开头的类(case)python_classes = Test* Check*
check_
开头和 test_
开头的方法(story)python_functions = check_* test_*
;就不用手动添加了
addopts = -vs --alluredir=./result
testpaths = demo demo3
norecursedirs = result md2
pip install pytest-ordering
pip install pytest-xdist
pytest -n auto
或 pytest -n NUMCPUS
即内核数def pytest_collection_modifyitems(session: "Session", config: "Config", items: List["Item"]
) -> None:print(items)# 单步调试可以发现:我们需要改每个 item(用例) 的 name 和 nodeID 两个编码for item in items:item.name = item.name.encode('utf-8').decode('unicode-escape')item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
# hook 函数,添加命令行参数
def pytest_addoption(parser):mygroup = parser.getgroup("Roy") # 参数组mygroup.addoption("--env", default='allen', dest='env', help='set your env')# 用 fixture 过滤参数
@pytest.fixture(scope='session')
def cmdoption(request):myenv = request.config.getoption("--env", default='allen')if myenv == 'roy':datapath = "datas/roy/data.yml"elif myenv == 'allen':datapath = "datas/allen/data.yml"else:datapath = "datas/data.yml"with open(datapath) as f:data = yaml.safe_load(f)return myenv, data
env:ip: 127.0.0.1port: 8999
pytest --env 'roy' .\test_conf.py -vs
,能看到对应输出pytest --help
也能看到参数介绍pip install setuptool
pip install wheel
,一个是打包的,一个是压缩的python -m build
twine
工具;都是参考上面那个教程,英文的慢慢看site-packages/_pytest/hookspec.py
文件中,pip 安装的包都放在 site-packages