pytest: Python的一个单元测试框架,基于UnitTest二次开发,语法上更加简洁,可以用来做Python开发项目的单元测试,UI自动化、接口自动化测试等,有很多的插件访问Pytest插件汇总,对Pytest进行扩展。
pytest是一个框架,它使构建简单且可伸缩的测试变得容易。测试具有表达性和可读性,不需要样板代码。在几分钟内开始对应用程序或库进行小的单元测试或复杂的功能测试。 -- 来自Pytest官方文档(由谷歌翻译)
https://docs.pytest.org/en/latest/parametrize.html
安装
pip install pytest
文件名称规则
与UnitTest类似,测试文件名称需要以test_文件名.py
(推荐使用这种)或者文件名_test.py
格式。
测试类的名称:Test类名
来定义一个测试类
测试方法(函数):test_方法名
来定义一个测试方法
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_01.py
@ide: PyCharm
@time: 2020/7/27
"""
# 测试类TestNumber
class TestNumber(object):
def __int__(self):
pass
def test_sum(self):
# assert进行断言
assert 1 == 2
def test_str(self):
# isinstance 判断对象是否为对应类型
assert isinstance(1, int)
运行测试
方法1:在当前目录下使用命令行,输入pytest
将会执行该目录下所有test_*.py
文件
(venv) bogon:pytest_study zy7y$ pytest
========================================================================= test session starts =========================================================================
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/zy7y/PycharmProjects/pytest_study
collected 3 items
test_01.py F. # F.为测试失败 [ 66%]
test_02.py . # . 为测试通过 [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestNumber.test_sum _________________________________________________________________________
self = <test_01.TestNumber object at 0x1110fb580>
def test_sum(self):
# assert进行断言
> assert 1 == 2
E assert 1 == 2
test_01.py:19: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_01.py::TestNumber::test_sum - assert 1 == 2
===================================================================== 1 failed, 2 passed in 0.07s =====================================================================
方法2: 在当前目录下使用命令行,输入pytest test_*.py
指定运行某个测试文件
(venv) bogon:pytest_study zy7y$ pytest test_01.py
F. [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestNumber.test_sum _________________________________________________________________________
self = <test_01.TestNumber object at 0x10824aac0>
def test_sum(self):
# assert进行断言
> assert 1 == 2
E assert 1 == 2
test_01.py:19: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_01.py::TestNumber::test_sum - assert 1 == 2
1 failed, 1 passed in 0.06s
方法3:在测试文件下使用pytest.main()
方法来运行.
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_01.py
@ide: PyCharm
@time: 2020/7/27
"""
# 测试类TestNumber
class TestNumber(object):
def __int__(self):
pass
def test_sum(self):
# assert进行断言
assert 1 == 2
def test_str(self):
assert isinstance(1, int)
if __name__ == '__main__':
import pytest
# 相当于在命令行当前目录中执行了 pytest
pytest.main()
# 执行pytest -q test_01.py : -q 简短输出测试信息
pytest.main(['-q', 'test_01.py'])
方法4:使用Pycharm修改单元测试框架来启动单个测试文件.
![image-20200727155705065](/Users/zy7y/Library/Application Support/typora-user-images/image-20200727155705065.png)
![image-20200727155814350](/Users/zy7y/Library/Application Support/typora-user-images/image-20200727155814350.png)
临时目录
pytest提供内置的fixtures / function参数来请求任意资源,例如唯一的临时目录:
在测试函数签名中列出名称tmpdir,pytest将在执行测试函数调用之前查找并调用夹具工厂来创建资源。 在测试运行之前,pytest创建一个“每次测试调用唯一”临时目录
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_02.py
@ide: PyCharm
@time: 2020/7/27
"""
def test_02():
assert 1 == 1
# 临时目录
def test_neddsfiles(tmpdir):
# 打印临时目录地址
print(tmpdir) # tmpdir = local('/private/var/folders/m4/gq5zy91j62x8qx_msq5mdbxw0000gn/T/pytest-of-zy7y/pytest-7/test_neddsfiles0')
# 0 和 None 、 '' 在python中属于 False
assert None
pytest其他命令
命令行中执行:
pytest --fixtures # 显示内置和自定义 fixtures
pytest -h # 查看帮助
pytest -x # 第一次故障后停止测试
pytest --maxfail=2 # 2次失败后停止
运行测试方法:
pytest test_*.py # 在模块中运行测试(运行整个.py下的测试类、方法、函数)
pytest testing/ # 在工作目录运行测试(运行指定文件目录下的所有测试类、方法、函数)
pytest -k "Number and not str" # 关键字运行
这将运行包含与给定字符串表达式(大小写不敏感)匹配的名称的测试,其中可以包括使用文件名、类名和函数名作为变量的Python操作符。上面的示例将运行TestNumber。测试一些东西,但不运行test_str测试方法。
pytest test_02.py::test_02 # 在模块中运行测试,就是说运行test_02.py文件中test_02的函数
pytest test_01.py::TestNumber::test_sum # 运行模块下指定测试类的测试方法
pytest -m slow # 将运行所有用@pytest.mark.slow装饰的测试
pytest --pyargs pkg.testing # 运行文件名称为testing的包下面所有的测试文件
fixture(依赖注入)
注意: Pytest对于每个fixture只会缓存一个实例,这意味着如果使用参数化的fixture,pytest可能会 比定义的作用域更多次的调用fixture函数(因为需要创建不同参数的fixture)
-
作为函数入参的fixture
""" @project: pytest_study @author: zy7y @file: test_03.py @ide: PyCharm @time: 2020/7/27 """ import pytest # 其他测试函数传入这个函数名称时会得到一个实例对象,且一直都是这个实例对象 @pytest.fixture def get_driver(): from selenium import webdriver # 返回一个操控Chrome浏览器的驱动实例 driver driver = webdriver.Chrome() return driver def test_browse(get_driver): get_driver.get("http://www.baidu.com") print(id(get_driver)) def test_close(get_driver): get_driver.close() print(id(get_driver))
-
在类/模块/中共享fixture实例
将fixture函数放在独立的
conftest.py
文件中,可以达到在多个测试模块中访问使用这个fixture;将scope="module"参数添加到@pytest.fixture中# conftest.py #!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: pytest_study @author: zy7y @file: conftest.py @ide: PyCharm @time: 2020/7/27 """ import pytest @pytest.fixture(scope='module') def browse_driver(): from selenium import webdriver # 返回一个操控Chrome浏览器的驱动实例 driver driver = webdriver.Chrome() return driver
# test_04.py #!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: pytest_study @author: zy7y @file: test_04.py @ide: PyCharm @time: 2020/7/27 """ def test_open_browse(browse_driver): browse_driver.get("https://www.baidu.com") def test_open_blog(browse_driver): import time time.sleep(3) browse_driver.get("https://www.baidu.com") print(browse_driver.window_handles) browse_driver.close()
-
fixture结束/执行清理代码
# 修改后的conftest.py import pytest @pytest.fixture(scope='module') def browse_driver(): # from selenium import webdriver # # 返回一个操控Chrome浏览器的驱动实例 driver # driver = webdriver.Chrome() # return driver # 调用结束/执行清理代码 from selenium import webdriver driver = webdriver.Chrome() # 调用前 会实例一个 driver, 然后通过yield 返回 yield driver # 用例结束后使用close关闭driver driver.close()
# test_05.py #!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: pytest_study @author: zy7y @file: test_05.py @ide: PyCharm @time: 2020/7/27 """ def test_open_browse(browse_driver): browse_driver.get("https://www.baidu.com") assert browse_driver.title == '百度一下,你就知道' def test_open_blog(browse_driver): import time time.sleep(3) browse_driver.get("https://www.cnblogs.com/zy7y/") assert 'zy7y' in browse_driver.title
-
fixture固件作用域
在定义固件时,通过 scope 参数声明作用域,可选项有: function: 函数级,每个测试函数都会执行一次固件; class: 类级别,每个测试类执行一次,所有方法都可以使用; module: 模块级,每个模块执行一次,模块内函数和方法都可使用; session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。 package,表示fixture函数在测试包(文件夹)中第一个测试用例执行前和最后一个测试用例执行后执行一次
# 启动顺序 session(单元测试集 -> 模块 -> 类 - >方法)
用例初始化(UnitTest的类前运行等方法)
模块级别(setup_module/teardown_module) 全局的 函数级别(setup_function/teardown_function) 只对函数用例生效(不在类中的) 类级别 (setup_class/teardown_class) 只在类中前后运行一次(只在类中才生效) 方法级别 (setup_method/teardown_method) 开始后与方法始末(在类中) 类内用例初始化结束 (setup/teardown) 运行在测试用例的前后
import pytest
def setup_module():
print('
setup_module 执行')
def teardown_module():
print('
teardown_module 执行')
def setup_function():
"""函数方法(类外)初始化"""
print('setup_function 执行')
def teardown_function():
"""函数方法(类外)初始化"""
print('
teardown_function 执行')
def test_in_class():
"""类(套件)外的测试用例"""
print('类外测试用例')
class Test_clazz(object):
"""测试类"""
def setup_class(self):
print('setup_class 执行')
def teardown_class(self):
print('teardown_class 执行')
def setup_method(self):
print('setup_method 执行')
def teardown_method(self0):
print('teardown_method 执行')
def setup(self):
print('
setup 执行')
def teardown(self):
print('
teardown 执行')
def test_case001(self):
"""测试用例一"""
print('测试用例一')
def test_case002(self):
"""测试用例二"""
print('测试用例二')
跳过测试函数
实现部分场景该测试函数不需要执行
# test_07.py
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_07.py
@ide: PyCharm
@time: 2020/7/27
"""
import pytest
import sys
# 这将强制跳过
@pytest.mark.skip(reason="该测试函数将被跳过")
def test_sum():
assert 1 != 2
def test_count():
assert 1 == 0
# 满足条件才跳过
@pytest.mark.skipif(sys.version_info < (3, 6), reason="版本低于3.6将跳过不执行!")
def test_num():
assert 1 + 3 == 5
# 标记测试用例期望失败
@pytest.mark.xfail(reason='期望这个测试是失败的')
def test_func01():
assert 1 == 1
参数化-UnitTest的DDT
内置的pytest.mark.parametrize装饰器可以用来对测试函数进行参数化处理。
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_08.py
@ide: PyCharm
@time: 2020/7/27
"""
import pytest
"""
'username, password, expect' 来传递 需要使用到的行参名
[('admin', '123456', '登录成功'), ('admin', '111111', '密码错误')] 来传递对应的实参,这个列表中每一个元组执行一次
ids=["正常登录测试用例标题", "密码错误登录测试用例"] 来传递对应每一次测试的用例标题
这个文件中执行了两次
"""
@pytest.mark.parametrize('username, password, expect',
[('admin', '123456', '登录成功'),
('admin', '111111', '密码错误')], ids=["正常登录测试用例标题", "密码错误登录测试用例"])
def test_login(username, password, expect):
if username == 'admin' and password == '123456':
assert expect == '登录成功'
else:
assert expect == '密码错误'
# 这将实现两个装饰器的参数组合并且用来测试
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
assert x + y == 5
运行脚本,会发生一个字符编码的错误:
解决方法是在 项目根目录下建立一个
pytest.ini
文件并写入[pytest] disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True