pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:
pytest-selenium
(集成selenium)、pytest-html
(完美html测试报告生成)、pytest-rerunfailures
(失败case重复执行)、pytest-xdist
(多CPU分发)等官方文档位置:https://docs.pytest.org/en/7.1.x/
这里使用的是poetry
来进行安装的,poetry使用方法
poetry add pytest # 添加pytest
poetry add pytest-html # 添加 html 测试报告生成
pytest --version # 检测是否安装成功
创建一个test_*.py
或者*_test..py
的文件,运行的时候,pytest可以自动检测这些测试文件:
# content of test_sample.py
def func(x):
return x + 1
def test_answer():
assert func(3) == 5
你可以使用
assert
验证测试期望的声明。皮特试验 Advanced assertion introspection 将智能地报告断言表达式的中间值,以便避免使用多个名称 of JUnit legacy methods .
然后,我们就使用,pytest提供的命令开始进行测试:
pytest tests --html report.html # --html 指定html输出的位置,tests是存放测试文件的文件夹
pytest
将运行窗体测试的所有文件_ *.py or * _当前目录及其子目录中的test.py
运行结果:
我们使用 raises 助手来断言某些代码引发异常:
# content of test_sysexit.py
import pytest
def f():
raise SystemExit(1)
def test_mytest():
with pytest.raises(SystemExit):
f()
# 运行测试
# pytest -q
以“安静”报告模式执行测试功能: 它不会显示其为程序的异常,测试正常通过。
pytest
中的-q
参数代表保持输出简短 ,只输出程序异常的日志,其余的不输出。
一旦开发了多个测试,您可能需要将它们分组到一个类中。pytest使创建包含多个测试的类变得很容易:
# content of test_class.py
class TestClass:
def test_one(self):
x = "this"
assert "h" in x
def test_two(self):
x = "hello"
assert hasattr(x, "check")
pytest
发现以下所有测试 Conventions for Python test discovery ,所以它发现test_
前缀函数。没有必要对任何东西进行子类化,但是要确保在类前面加上Test
否则将跳过该类。我们只需传递其文件名即可运行该模块:pytest -q test_class.py
第一次测试通过,第二次失败。您可以很容易地看到断言中的中间值,以帮助您理解失败的原因。
将测试分组在类中是有益的,原因如下:
在类中对测试分组时需要注意的是,每个测试都有一个唯一的类实例。让每个测试共享同一个类实例将非常不利于测试隔离,并且会导致不良的测试实践。概述如下:
# content of test_class_demo.py
class TestClassDemoInstance:
value = 0
def test_one(self):
self.value = 1 # 只有静态函数才可以修改类属性,这里是修改失败的
assert self.value == 1
def test_two(self):
assert self.value == 1
pytest -k TestClassDemoInstance -q
- 在类级别添加的属性是类属性,因此它们将在测试之间共享
pytest
提供内置固定装置/函数参数来请求任意资源,例如唯一的临时目录:
# content of test_tmp_path.py
def test_needsfiles(tmp_path):
print(tmp_path)
assert 0
# pytest -q test_tmp_path.py # 运行
列出名字
tmpdir
在测试函数签名和pytest
将在执行测试函数调用之前查找并调用fixture工厂以创建资源。使用
pytest --fixtures
命令查看有哪些ficture
工厂。
这里使用的是poetry
进行快速生成目录结构的,poetry使用方法。
pytest
实现以下标准测试发现:
testpaths
(如果已配置)或当前目录开始。或者,命令行参数可用于目录、文件名或节点 ID 的任意组合。norecursedirs
。test_*.py
或文件,由它们的测试包名称*_test.py
导入。test
类外的前缀测试函数或方法test
前缀测试类中的前缀测试函数或方法Test
(没有__init__
方法)在 Python 模块中,pytest
还使用标准 unittest.TestCase子类化技术发现测试。
如果您有许多功能测试或出于其他原因希望将测试与实际应用程序代码分开(通常是一个好主意),则将测试放入实际应用程序代码之外的额外目录可能会很有用:
pyproject.toml
src/
mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
...
快速生成的方法:
poetry new --src mypkg
运行测试的方法:
pytest python -m pytest
通常,但特别是如果您使用默认导入模式
prepend
,强烈建议使用src
布局。在这里,您的应用程序根包位于根目录的子目录中,src/mypkg/
而不是mypkg
。
如果您在测试和应用程序模块之间有直接关系并且希望将它们与您的应用程序一起分发,则将测试目录内联到您的应用程序包中很有用:
pyproject.toml
[src/]mypkg/
__init__.py
app.py
view.py
test/
__init__.py
test_app.py
test_view.py
...
运行方法:
pytest --pyargs mypkg
pytest
将发现mypkg
安装位置并从那里收集测试,这样可以很轻松的运行测试代码。
Pytest 支持多种从命令行运行和选择测试的方法。
# 在模块中运行测试
pytest test_mod.py
# 在目录中运行测试
pytest testing/
# 通过关键字表达式运行测试
pytest -k "MyClass and not method"
第三种方式:
- 这将运行包含与给定字符串表达式匹配的名称(不区分大小写)的测试,其中可以包括使用文件名、类名和函数名作为变量的 Python 运算符。上面的例子会运行
TestMyClass.test_something
,但不会TestMyClass.test_method_simple
。
按节点 ID 运行测试
每个收集到的测试都被分配一个唯一的nodeid
,它由模块文件名和后面的说明符组成,如类名、函数名和参数化的参数,用::
字符分隔。
# 要在模块中运行特定测试:
pytest test_mod.py::test_func
# 在命令行中指定测试方法的另一个示例:
pytest test_mod.py::TestClass::test_method
通过标记运行实例
pytest -m slow
将运行所有用
@pytest.mark.slow
装饰器装饰的测试。
从包运行测试
pytest --pyargs pkg.testing
这将导入
pkg.testing
并使用其文件系统位置来查找和运行测试。
获取版本信息等等
pytest --version # shows where pytest was imported from
pytest --fixtures # show available builtin function arguments
pytest -h | --help # show help on command line and config file options
分析测试执行持续时间
要获得超过 1.0 秒的最慢 10 个测试持续时间的列表
pytest --durations=10 --durations-min=1.0
通过python解释器调用测试
python -m pytest [...] # 后面传递的参数和上面的类似
pytest
您可以直接从 Python 代码调用:
retcode = pytest.main()
这就像您从命令行调用“pytest”一样。它不会引发SystemExit
而是返回退出代码。您可以传入选项和参数:
retcode = pytest.main(["-x", "mytestdir"])
导入其他插件:
# content of myinvoke.py
import pytest
import sys
class MyPlugin:
def pytest_sessionfinish(self):
print("*** test run reporting finishing")
if __name__ == "__main__":
sys.exit(pytest.main(["-qq"], plugins=[MyPlugin()]))
调用
pytest.main()
将导致导入您的测试和它们导入的任何模块。由于 python 导入系统的缓存机制,pytest.main()
从同一进程进行后续调用不会反映调用之间对这些文件的更改。因此,pytest.main()
不建议从同一进程多次调用(例如,为了重新运行测试)。
pytest
允许您使用标准 Pythonassert
来验证 Python 测试中的期望值和值。例如,您可以编写以下内容:
# content of test_assert1.py
def f():
return 3
def test_function():
"""如果失败,可以查看函数的返回值"""
assert f() == 4
assert a % 2 == 0, "value was odd, should be even" # 指定自定义的异常断言信息
为了编写有关引发异常的断言,您可以 pytest.raises()
像这样使用上下文管理器:
def test_recursion_depth():
with pytest.raises(RuntimeError) as excinfo:
def f():
f()
f()
assert "maximum recursion" in str(excinfo.value) # excinfo保存着异常信息
excinfo
是一个ExceptionInfo
实例,它是引发的实际异常的包装器。感兴趣的主要属性.type
是.value
和.traceback
。
您可以将match
关键字参数传递给上下文管理器,以测试正则表达式是否匹配异常的字符串表示形式(类似于TestCase.assertRaisesRegex
方法 from unittest
):
import pytest
def myfunc():
raise ValueError("Exception 123 raised")
def test_match():
with pytest.raises(ValueError, match=r".* 123 .*"):
myfunc()
也可以为pytest.mark.xfail
指定一个“raises”参数 ,它以一种更具体的方式检查测试是否失败,而不仅仅是引发任何异常:
@pytest.mark.xfail(raises=IndexError)
def test_f():
f()
pytest.raises()
对于您正在测试自己的代码故意引发的异常的情况, 使用可能会更好,而使用@pytest.mark.xfail
检查功能可能更适合记录未修复的错误(测试描述“应该”发生什么)或依赖项中的错误
在基本层面上,测试函数通过将它们声明为参数来请求它们所需的fixtures
。
当 pytest 开始运行测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数具有相同名称的fixtures
。一旦 pytest 找到它们,它就会运行这些固定装置,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
在这个例子中,
test_fruit_salad
需要fruit_bowl
作为依赖,当 pytest 看到这个时,它将执行夹具函数并将它返回的对象 作为参数传递。
注意:
固定装置可以嵌套调用
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
一个测试函数或者固定装置可以使用多个固定装置
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def second_entry():
return 2
# Arrange
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
# Arrange
@pytest.fixture
def expected_list():
return ["a", 2, 3.0]
def test_string(order, expected_list):
# Act
order.append(3.0)
# Assert
assert order == expected_list
固定装置可以重复使用
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
在同一个测试期间,也可以多次请求夹具,并且 pytest 不会为该测试再次执行它们。这意味着我们可以在依赖于它们的多个固定装置中请求 固定装置(甚至在测试本身中再次请求固定装置),而无需多次执行这些固定装置。
自动使用固定装置
# contents of test_append.py
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
在此示例中,
append_first
夹具是自动使用夹具。因为它是自动发生的,所以两个测试都会受到它的影响,即使两个测试都没有 请求它。这并不意味着他们不能被请求;只是没有必要。
需要网络访问的设备取决于连接性,并且通常创建起来很耗时。我们可以scope="module"
在调用中添加一个参数来 @pytest.fixture
添加一个smtp_connection
固定装置,负责创建到一个预先存在的 SMTP 服务器的连接,每个测试模块只调用一次(默认是每个测试函数调用一次)。因此,测试模块中的多个测试函数将接收相同的smtp_connection
夹具实例,从而节省时间。scope
的可能值为:function
、class
、module
、package
、session
下一个示例将fixture函数放入一个单独的conftest.py
文件中,以便目录中多个测试模块的测试可以访问fixture函数:
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) # 这里也可以使用yield来返回参数,进行后续的内容
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
更加详细的使用方式,可以到官网自行查看
# content of test_server.py
import pytest
@pytest.mark.webtest
def test_send_http():
pass # perform some webtest test for your app
def test_something_quick():
pass
def test_another():
pass
class TestClass:
def test_method(self):
pass
然后,您可以将测试运行限制为仅运行标记为的测试webtest
:
pytest -v -m webtest
pytest -v -m "not webtest" # 运行除webtest之外的所有测试
pytest -v test_server.py::TestClass::test_method
pytest -v test_server.py::TestClass test_server.py::test_send_http
根据名称选择
您可以使用-k
命令行选项来指定一个表达式,该表达式在测试名称上实现子字符串匹配,而不是在提供的标记上实现完全匹配-m
。这使得根据名称选择测试变得容易:
pytest -v -k http
pytest -k "not send_http" -v
pytest -k "http or quick" -v
更高级的用法,可以到官方文档中自己学习,https://docs.pytest.org/en/7.1.x/index.html