Pytest文档学习笔记
命令行参数
- -v: 输出详细信息
- -s: 输出调试信息(如 print)
- -x: 遇到失败的测试用例立即停止测试
- -m: 执行特定的标记(mark)
pytest -m "mark1 or mark2
- -k: 执行包含特定字符串的测试用例
- –maxfail=num: 当失败测试用例数量达到num时,停止测试
- –ff (–failed-first): 之前运行失败的测试用例会首先被运行,然后才运行其他的测试用例
- –lf (–last-failed): 仅运行上次运行失败的测试用例
- –showlocals(简写-l): 调试失败的测试用例。
- –tb=native:这个参数用于在显示测试失败时使用本机(native)的回溯(traceback)格式。
- –assert=plain:这个参数用于设置断言失败时的输出格式为简单模式。简单模式可能对某些情况下的补丁操作更友好。
- –capture=no:这个参数用于禁用输出捕获。在某些情况下,输出捕获可能受到补丁操作的影响,禁用捕获可以帮助诊断问题。
fixtures
What is fixtures
在测试中,
fixtures
为测试提供了定义的、可靠的和一致的上下文。这可能包括环境(例如配置了已知参数的数据库)或内容(例如数据集)。当我们运行一个测试时,pytest 会在函数参数中寻找同名的 fixture,一旦找到了某一个fixture,就会去获取相应的 fixture 的返回值,将这个返回值传递给测试函数
使用方法,在函数前加注解(装饰器)
@pytest.fixtures
example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import pytest
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def my_fruit():
return Fruit("apple")
def fruit_basket(my_fruit):
return [Fruit("banana"), my_fruit]
def test_my_fruit_in_basket(my_fruit, fruit_basket):
assert my_fruit in fruit_basket测试文件中可以多个函数都有
fixture
注解,一个带有fixture
注解的函数也可以去调用另外一个fixture
函数
Improvements over xUnit-style setup/teardown functions
显式命名与声明的使用:
pytest 的 fixture 具有明确的名称,并且通过从测试函数、模块、类或整个项目中声明其使用来激活。
这意味着可以清楚地知道每个 fixture 的作用,并且只在需要时调用它们。
1
2
3
4
5
6
7
8
9# Fixture with explicit name 'database'
def database():
return Database()
# Using the 'database' fixture in a test function
def test_database_operations(database):
assert database.query() == expected_result模块化实现:
每个 fixture 的名称触发一个 fixture 函数,这种模块化的实现允许一个 fixture 函数使用其他 fixtures。
这种设计使得你可以构建和组织自己的 fixtures,将它们组合在一起以创建更复杂的测试环境。
1
2
3
4
5
6
7
8
9
def user():
return User()
def authenticated_user(user):
# Using the 'user' fixture inside the 'authenticated_user' fixture
return authenticate_user(user)规模化的管理:
从简单的单元测试到复杂的功能测试,pytest 的 fixture 管理可以进行扩展。
可以根据配置和组件选项对 fixtures 和测试进行参数化,或者在不同的作用域(函数、类、模块、整个测试会话)中重复使用 fixtures。
1
2
3
4
5
6
7
def feature_enabled(request):
return request.param
def test_feature(feature_enabled):
assert feature_enabled简化拆卸逻辑:
可以轻松、安全地管理拆卸逻辑,而不管使用了多少个 fixtures,而不需要手动处理错误或精心安排清理步骤的顺序。
能够更容易地处理测试后的清理工作,无论测试使用了多少 fixtures。
1
2
3
4
5
6
7
8
9
def setup_and_teardown():
# Setup code
yield
# Teardown code
def test_with_teardown(setup_and_teardown):
# Test code支持 xUnit 风格的设置:
- pytest 仍然支持 xUnit 风格的设置。你可以混合使用两种风格,逐步从经典的方式过渡到新的 fixture 风格,也可以从现有的 unittest.TestCase 风格或基于 nose 的项目中开始。
1
2
3
4
5
6
7
8
9
10
11
12import unittest
class MyTestCase(unittest.TestCase):
def setUp(self):
# Classic setup code
def test_something(self):
# Test code
def tearDown(self):
# Classic teardown code
Fixtures error
对于测试用例中的
fixtures
最好以线性排序,以确保能够清晰地了解哪个 fixture 先执行,哪个后执行。然而,如果较早的 fixture 存在问题并引发异常,pytest 会停止执行该测试,并标记为具有错误。
当一个测试被标记为有错误时,并不意味着测试失败,而是表示测试甚至无法尝试,因为它所依赖的某个组件出现了问题。
所以,在编写测试用例时尽量减少不必要的依赖关系的重要性。通过减少依赖,可以确保不相关的问题不会导致我们无法得知测试用例的实际问题,从而帮助更准确地定位和解决测试中的异常情况。
example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import pytest
def order():
return []
def append_first(order):
order.append(1)
def append_second(order, append_first):
order.extend([2])
def append_third(order, append_second):
order += [3]
def test_order(order):
assert order == [1, 2, 3]假如,由于某种原因,在执行
order.append(1)
的时候出错了,那么我们就无法知道order.append([2])
和order += [3]
是否也出错了,当append_first
抛出异常之后,pytest将不再为test_order
执行任何的fixtures
,同样也不会执行test_order
,唯一执行的只有order
和append_first
Built-in fixtures
- pytest 提供了一些内置的 fixtrue 函数:
tmpdir 和 tempdir_factory
内置的
tmpdir
和tmpdir_factory
负责在测试开始运行前创建临时文件目录,并在测试结束后删除。其主要特性如下所示:如果测试代码要对文件进行读写操作,可以使用
tmpdir
或tmpdir_factory
来创建文件或目录,单个测试使用tmpdir
,多个测试使用tmpdir_factory
tmpdir
的作用范围为函数(function)级别,tmpdir_factory
作用范围是会话(session)级别
example
1
2
3
4
5
6
7
8
9
10
11import pytest
def test_tmpDir(tmpdir):
tmpfileA = tmpdir.join("testA.txt")
tmpSubDir = tmpdir.mkdir("subDir")
tmpfileB = tmpSubDir.join("testB.txt")
tmpfileA.write("this is pytest tmp file A")
tmpfileB.write("this is pytest tmp file B")
assert tmpfileA.read() == "this is pytest tmp file A"
assert tmpfileB.read() == "this is pytest tmp file B"tmpdir
的作用范围是函数级别,所以只能针对测试函数使用 tmpdir 创建文件或目录。如果 fixture 作用范围高于函数级别(类、模块、会话),则需要使用tmpdir_factory
。tmpdir 与 tmpdir_factory 类似,但提供的方法有一些不同,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14import pytest
def test_tmpDir(tmpdir_factory):
baseTmpDir = tmpdir_factory.getbasetemp()
print(f"\nbase temp dir is :{baseTmpDir}")
tmpDir_factory = tmpdir_factory.mktemp("tempDir")
tmpfileA = tmpDir_factory.join("testA.txt")
tmpSubDir = tmpDir_factory.mkdir("subDir")
tmpfileB = tmpSubDir.join("testB.txt")
tmpfileA.write("this is pytest tmp file A")
tmpfileB.write("this is pytest tmp file B")
assert tmpfileA.read() == "this is pytest tmp file A"
assert tmpfileB.read() == "this is pytest tmp file B"getbasetemp()
用于返回该会话使用的根目录,pytest-NUM
会随着会话的增加而进行自增,pytest 会记录最近几次(官方文档给的默认值为3次)会话使用的根目录,更早的根目录记录则会被清理掉。可通过配置tmp_path_retention_count
和tmp_path_retention_policy
来更改:1
2
3
4
5
6[pytest]
tmp_path_retention_count = 3
tmp_path_retention_policy = "all"
; all: retains directories for all tests, regardless of the outcome.
; failed: retains directories only for tests with outcome error or failed.
; none: directories are always removed after each test ends, regardless of the outcome.另外也可在命令行指定临时目录,如下所示:
1
pytest --basetemp=dir
如何在其他作用范围内使用临时目录
tmpdir_factory
的作用范围是会话级别的,tmpdir
的作用范围是函数级别的。如果需要模块级别或类级别的作用范围的目录,该如何解决?针对这种情况,可以利用tmpdir_factory
再创建一个fixture
example
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
32
33
34
35
36
37
38# content of conftest.py
import json
import pytest
def readJson(tmpdir_factory):
jsonData={
"name":"zhangsan",
"age":28,
"locate":"shangahi",
"loveCity":{"shanghai":"shanghai",
"wuhai":"hubei",
"shenzheng":"guangdong"
}
}
file=tmpdir_factory.mktemp("jsonTemp").join("tempJSON.json")
with open(file,"w",encoding="utf8") as fo:
json.dump(jsonData,fo,ensure_ascii=False)
# print(f"base dir is {tmpdir_factory.getbasetemp()}")
return file
# content of test_module_temdir.py
import json
def test_getData(readJson):
with open(readJson,"r",encoding="utf8") as fo:
data=json.load(fo)
assert data.get("name")=="zhangsan"
def test_getLoveCity(readJson):
with open(readJson,"r",encoding="utf8") as fo:
data=json.load(fo)
getCity=data.get("loveCity")
for k,v in getCity.items():
assert len(v)>0
tmp_path 和 tmp_path_factory
tmp_path
是一个pathlib.Path
对象.提供了一个临时目录的路径,用于在测试过程中创建和操作临时文件和目录。这个 fixture 在测试中经常用于执行文件操作而不影响实际文件系统。
使用方法:
1
2
3
4
5
6
7
8
9
10
11
12# content of test_tmp_path.py
import pathlib
CONTENT = "content"
def test_create_file(tmp_path: pathlib.WindowsPath):
d = tmp_path / "sub"
d.mkdir()
p = d / "hello.txt"
p.write_text(CONTENT, encoding="utf-8")
assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1
assert 0tmp_path
与tmpdir
的区别:tmp_path
是一个pathlib.Path
对象,表示一个临时目录的路径。通常用于执行文件和目录操作。(python standard object)tmpdir
是一个py.path.local
对象,也表示一个临时目录的路径。它提供了一些额外的方法用于文件操作,与pathlib.Path
有一些不同之处。 (pytest custom designed object)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import py
def test_tmp_path(tmp_path: pathlib.WindowsPath, tmpdir: py.path.LocalPath):
# tmp_path 是 pathlib.Path 对象,tmpdir 是 py.path.local 对象
print(f"\n{type(tmp_path)}") # <class 'pathlib.WindowsPath'>
print(f"{type(tmpdir)}") # <class '_pytest._py.path.LocalPath'>
# 使用 tmp_path 进行文件操作
tmp_file_path = tmp_path / "example.txt"
tmp_file_path.write_text("Hello, World!", encoding="utf-8")
assert tmp_file_path.read_text(encoding="utf-8") == "Hello, World!"
# 使用 tmpdir 进行文件操作
tmp_file_dir = tmpdir / "example.txt"
tmp_file_dir.write_text("Hello, Pytest!", encoding="utf-8")
assert tmp_file_dir.read_text(encoding="utf-8") == "Hello, Pytest!"tmp_path_factory
和tmpdir_factory
:tmp_path_factory
是一个工厂函数,用于创建tmp_path
。每次调用 tmp_path_factory 时,都会返回一个新的pathlib.Path
对象,表示一个新的临时目录。tmpdir_factory
是一个工厂函数,用于创建tmpdir
。每次调用 tmpdir_factory 时,都会返回一个新的py.path.local
对象,表示一个新的临时目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from pytest import TempPathFactory, TempdirFactory
def test_tmp_path_factory(tmp_path_factory: TempPathFactory, tmpdir_factory: TempdirFactory):
# tmp_path_factory 和 tmpdir_factory 是工厂函数
print(f"\n{type(tmp_path_factory)}") # <class '_pytest.tmpdir.TempPathFactory'>
print(type(tmpdir_factory)) # <class '_pytest.legacypath.TempdirFactory'>
# 创建新的临时目录,返回 pathlib.Path 对象
tmp_path = tmp_path_factory.mktemp("test_dir")
assert isinstance(tmp_path, pathlib.Path)
# 创建新的临时目录,返回 py.path.local 对象
tmpdir = tmpdir_factory.mktemp("test_dir")
assert isinstance(tmpdir, py.path.local)
pytestconfig
内置的
pytestconfig
可以通过命令行参数、选项、配置文件、插件、运行目录等方式来控制 pytest。pytestconfig 是request.config
的快捷方式,在 pytest 中称之为 pytest 配置对象该函数可以配合
pytest_addoption
钩子函数一起使用example
1
2
3
4
5
6
7
8
9
10
11
12
13# content of conftest.py
def pytest_addoption(parser):
parser.addoption("--myopt",action="store_true",help="test boolean option")
parser.addoption("--foo",action="store",default="zhangsan",help="test stroe")
# content of test_pyteestconfig.py
import pytest
def test_myOption(pytestconfig):
print(f"--foo {pytestconfig.getoption('foo')}")
print(f"--myopt {pytestconfig.getoption('myopt')}")
# 执行:pytest -vs --myopt --foo zhangsan test_pytestconfig.py因为
pytestconfig
是一个 fixture 函数,所以也可以被其他 fixture 函数调用1
2
3
4
5
6
7
8
9
10
11
12
13import pytest
def foo(pytestconfig):
return pytestconfig.option.foo
def myopt(pytestconfig):
return pytestconfig.option.myopt
def test_fixtureForAddOption(foo,myopt):
print(f"\nfoo -- {foo}")
print(f"\nmyopt -- {myopt}")除了使用 pytestconfig 自定义之外,也可以使用内置的选项和 pytest 启动时的信息,如目录、参数等。
1
2
3
4
5
6
7
8def test_pytestconfig(pytestconfig):
print(f"args : {pytestconfig.args}")
print(f"ini file is : {pytestconfig.inifile}")
print(f"root dir is : {pytestconfig.rootdir}")
print(f"invocation dir is :{pytestconfig.invocation_dir}")
print(f"-q, --quiet {pytestconfig.getoption('--quiet')}")
print(f"-l, --showlocals:{pytestconfig.getoption('showlocals')}")
print(f"--tb=style: {pytestconfig.getoption('tbstyle')}")
cache
通常情况下,每个测试用例彼此都是独立的,互不影响。但有时,一个测试用例运行完成后,希望将其结果传递给下一个测试用例,这种情况下,则需要使用pytest内置的cache。
为记住上次测试失败的用例,pytest 存储了上一个测试会话中测试失败的信息,可以使用
--cache-show
标识来显示存储的信息。如果需要清空cache,可以在测试会话之前,传入–clear-cache标识即可
cache除了–lf和–ff两个标识之外,还可以使用其接口,如下所示:
1
2cache.get(key,default)
cache.set(key,value)参考代码
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
26import datetime
import time
import random
import pytest
# 创建一个fixture,记录测试的耗时,并存储到cache中
# 如果后面的测试耗时大于之前的2倍,就抛出超时异常。
# pytest -vs --cache-clear test_cache.py
def checkDuration(request,cache):
key = "duration/"+request.node.nodeid.replace(":","_")
startTime = datetime.datetime.now()
yield
endTime = datetime.datetime.now()
duration = (endTime-startTime).total_seconds()
lastDuration = cache.get(key,None)
cache.set(key,duration)
if lastDuration is not None:
errorString = "test duration over twice last duration"
assert duration <= 2 * lastDuration,errorString
def test_duration(t):
time.sleep(random.randint(0,5))
capsys
pytest内置的capsys主要有两个功能:
允许使用代码读取stdout和stderr
可以临时禁止抓取日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import sys
def greeting(name):
print(f"Hello,{name}")
def test_greeting(capsys):
greeting("Surpass")
out,err = capsys.readouterr()
assert "Hello,Surpass" in out
assert err == ""
def greeting_err(name):
print(f"Hello,{name}",file=sys.stderr)
def test_greeting(capsys):
greeting_err("Surpass")
out,err = capsys.readouterr()
assert "Hello,Surpass" in err
assert out == ""pytest 通常会抓取测试用例及被测试代码的输出。而且是在全部测试会话结束后,抓取到的输出才会随着失败的测试显示出来。
--s
参数可以关闭该功能,在测试仍在运行时就把输出直接发送到 stdout ,但有时仅需要其中的部分信息,则可以使用capsys.disable()
,可以临时让输出绕过默认的输出捕获机制,示例如下所示:1
2
3
4def test_capsysDisable(capsys):
with capsys.disabled():
print("\nalways print this information")
print("normal print,usually captured")
monkeypatch
monkey patch
可以在运行期间对类或模块进行动态修改。在测试中,monkey patch 常用于替换被测试代码的部分运行环境或装饰输入依赖或输出依赖替换成更容易测试的对象或函数。在 pytest 内置的 monkey patch 允许单一环境中使用,并在测试结束后,无论结果是失败或通过,所有修改都会复原。monkeypatch 常用的函数如下所示:1
2
3
4
5
6
7
8setattr(target, name, value=<notset>, raising=True): # 设置属性
delattr(target, name=<notset>, raising=True): # 删除属性
setitem(dic, name, value): # 设置字典中一个元素
delitem(dic, name, raising=True): # 删除字典中一个元素
setenv(name, value, prepend=None): # 设置环境变量
delenv(name, raising=True): # 删除环境变量
syspath_prepend(path) # 将path路径添加到sys.path中
chdir(path) # 改变当前的工作路径raising
参数用于指示 pytest 在记录不存在时,是否抛出异常setenv()
中的prepend
可以是一个字符,如果是这样设置,则环境变量的值就是 value+ prepend +
不建议对内置函数(如 open、compile 等)进行补丁(patch)操作,因为这可能会破坏 Pytest 的内部实现。如果确实无法避免这样的操作,可以尝试使用一些参数(–tb=native、–assert=plain 和 –capture=no)来减轻可能出现的问题,尽管并不保证能够完全解决。
在进行补丁操作时,需要注意对标准库函数和 pytest 使用的一些第三方库进行补丁可能会破坏 pytest 本身。因此,在这些情况下,建议使用
MonkeyPatch.context()
来将补丁限制在你想要测试的代码块中。1
2
3
4
5
6
7
8
9
10
11
12
13import functools
def test_partial(monkeypatch):
# 使用 MonkeyPatch.context() 来限制补丁的作用范围
with monkeypatch.context() as m:
# 在这个代码块中对 functools.partial 进行补丁
m.setattr(functools, "partial", 3)
# 在代码块内,functools.partial 被补丁为 3
assert functools.partial == 3
# 在代码块外,functools.partial 恢复为原来的值
assert functools.partial != 3看如下例子:
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
26import os
import json
defaulData={
"name":"Surpass",
"age":28,
"locate":"shangahi",
"loveCity":{"shanghai":"shanghai",
"wuhai":"hubei",
"shenzheng":"guangdong"
}
}
def readJSON():
path=os.path.join(os.getcwd(),"surpass.json")
with open(path,"r",encoding="utf8") as fo:
data=json.load(fo)
return data
def writeJSON(data:str):
path = os.path.join(os.getcwd(), "surpass.json")
with open(path,"w",encoding="utf8") as fo:
json.dump(data,fo,ensure_ascii=False,indent=4)
def writeDefaultJSON():
writeJSON(defaulData)writeDefaultJSON()
既没有参数也没有返回值,该如何测试?仔细观察函数,它会在当前目录中保存一个JSON文件,那就可以从侧面来进行测试。通常比较直接的方法,运行代码并检查文件的生成情况。如下所示:1
2
3
4
5def test_writeDefaultJSON():
writeDefaultJSON()
expectd = defaulData
actual = readJSON()
assert expectd == actual上面这种方法虽然可以进行测试,但却覆盖了原有文件内容。函数里面所传递的路径为当前目录,那如果将目录换成临时目录了,示例如下所示:
1
2
3
4
5
6
7def test_writeDefaultJSONChangeDir(tmpdir, monkeypatch):
tmpDir = tmpdir.mkdir("TestDir")
monkeypatch.chdir(tmpDir)
writeDefaultJSON()
expectd = defaulData
actual = readJSON()
assert expectd == actual以上这种虽然解决了目录的问题,那如果测试过程,需要修改数据,又该如何,示例如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def test_writeDefaultJSONChangeDir(tmpdir,monkeypatch):
tmpDir = tmpdir.mkdir("TestDir")
monkeypatch.chdir(tmpDir)
# 保存默认数据
writeDefaultJSON()
copyData = deepcopy(defaulData)
# 增加项
monkeypatch.setitem(defaulData, "hometown", "hubei")
monkeypatch.setitem(defaulData, "company", ["Surpassme","Surmount"])
addItemData = defaulData
# 再次保存数据
writeDefaultJSON()
# 获取保存的数据
actual = readJSON()
assert addItemData == actual
assert copyData != actual更多例子参考:
Monkeypatching functions
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# contents of test_module.py with source code and the test
from pathlib import WindowsPath
from pytest import MonkeyPatch
def getssh():
"""Simple function to return expanded homedir ssh path."""
return WindowsPath.home() / ".ssh" # C:/User/Admin/.ssh
def test_getssh(monkeypatch: MonkeyPatch):
print(type(monkeypatch)) # <class '_pytest.monkeypatch.MonkeyPatch'>
# mocked return function to replace Path.home
# always return '/abc'
def mockreturn():
return WindowsPath("/abc")
# Application of the monkeypatch to replace Path.home
# with the behavior of mockreturn defined above.
monkeypatch.setattr(WindowsPath, "home", mockreturn)
# Calling getssh() will use mockreturn in place of Path.home
# for this test with the monkeypatch.
x = getssh()
assert x == WindowsPath("/abc/.ssh")
Monkeypatching returned objects: building mock classes
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
32
33
34
35
36
37
38import requests
import pytest
from pytest import MonkeyPatch
def get_json(url):
"""Takes a URL, and returns the JSON."""
r = requests.get(url)
return r.json()
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
def json():
return {"mock_key": "mock_response"}
# monkeypatched requests.get moved to a fixture
def mock_response(monkeypatch: MonkeyPatch):
"""Requests.get() mocked to return {'mock_key':'mock_response'}."""
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse()
# apply the monkeypatch for requests.get to mock_get
# let mock_get() replace requests.get()
monkeypatch.setattr(requests, "get", mock_get)
def test_get_json(mock_response):
# get_json, which contains requests.get, uses the monkeypatch
result = get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"Monkeypatching environment variables
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
32
33
34import os
import pytest
from pytest import MonkeyPatch
def get_os_user_lower():
"""Simple retrieval function.
Returns lowercase USER or raises OSError."""
username = os.getenv("USER")
if username is None:
raise OSError("USER environment is not set.")
return username.lower()
def mock_env_user(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
def mock_env_missing(monkeypatch):
monkeypatch.delenv("USER", raising=False)
# notice the tests reference the fixtures for mocks
def test_upper_to_lower(mock_env_user):
assert get_os_user_lower() == "testinguser"
def test_raise_exception(mock_env_missing):
with pytest.raises(OSError):
_ = get_os_user_lower()
recwarn
- 内置的 recwarn 可以用来检查待测代码产生的警告信息。在 Python 中,我们可以添加警告信息,很像断言,但不阻止程序运行。假如在一份代码,想要停止支持一个已经过时的函数,则可以在代码中设置警告信息,示例如下所示:
1 | import warnings |
recwarn 的值就是一个警告信息列表,列表中的每个警告信息都有4个属性
category
、message
、filename
、lineno
。警告信息 在测试开始后收集,如果待测的警告信息在最后,则可以在信息收集前使用recwarn.clear()
清除不需要的内容。除 recwarn,还可以使用
pytest.warns()
来检查警告信息。示例如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13import warnings
import pytest
def depricateFunc():
warnings.warn("This function is not support after 3.8 version",DeprecationWarning)
def test_depricateFunc():
with pytest.warns(None) as warnInfo:
depricateFunc()
assert len(warnInfo)==1
w=warnInfo.pop()
assert w.category==DeprecationWarning
assert str(w.message) == "This function is not support after 3.8 version"
Sharing test data
如果要在测试用例中导入文件中的数据,推荐将这些文件数据加载到
fixtrues
中,这种做法利用 pytest 的自动缓存机制另一个做法是将数据文件放在一个
tests
文件夹下,要用到一个插件pytest-datadir
1
pip install pytest-datadir
- 假设目录结构如下:
1
2
3
4
5
6
project/
|-- tests/
| |-- test_hello.py
|-- data/
| |-- hello.txt- hello.txt 文件包含一些测试数据
1
2
3
4
5
6
7
8
9
10
11
12
13import pytest
# 使用 pytest-datadir 插件
def test_read_hello_file(datadir):
# datadir 是一个 fixture,它提供了测试数据目录的路径
file_path = datadir / 'hello.txt'
# 读取文件内容
with open(file_path, 'r') as file:
content = file.read()
# 在这个例子中,你可以根据文件内容执行相应的断言
assert "Hello, World!" in content
Fixture availability
从测试的角度来看,一个
fixture
只有在其定义的作用域内才能被测试请求到。如果一个fixture
在类内定义,那么只有在该类内的测试才能请求到它。但如果一个fixture
在模块的全局作用域内定义,那么该模块内的任何测试,即使是在类内定义的测试,都可以请求到它。一个
fixture
可以请求任何其他fixture
,无论它们在哪里定义,只要请求它们的测试能够看到所有涉及的fixture
。example
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
29import pytest
def order():
return []
def outer(order, inner):
order.append("outer")
class TestOne:
def inner(self, order):
order.append("one")
def test_order(self, order, outer):
assert order == ["one", "outer"]
class TestTwo:
def inner(self, order):
order.append("two")
def test_order(self, order, outer):
assert order == ["two", "outer"]
sharing fixtures across multiple files
conftest.py
文件可以用作在整个目录中提供fixtures
的手段。在 conftest.py 中定义的 fixtures 可以被该目录中的任何测试使用,而无需显式导入它们(pytest 会自动发现它们)。可以有多个嵌套的目录或包包含自定义测试,每个目录都可以有自己的
conftest.py
文件,其中定义了特定目录下的fixtures
,这些 fixtures 会添加到父目录中的 conftest.py 文件中定义的 fixtures 中,从而构建出一个层级的 fixtures 结构。假定有如下目录结构:
1
2
3
4
5
6
7
8
project/
|-- tests/
| |-- conftest.py # 这个文件定义了全局的 fixture
| |-- test_module1.py
| |-- subdirectory/
| |-- conftest.py # 这个文件定义了子目录专有的 fixture
| |-- test_module2.py各个目录下的文件内容如下:
tests/conftest.py
1
2
3
4
5
6
7
8
9import pytest
def order():
return []
def top(order, innermost):
order.append("top")tests/test_module1.py
1
2
3
4
5
6
7
8
9
10
11
12import pytest
def innermost(order):
order.append("innermost top")
def test_order(order, top):
assert order == ["innermost top", "top"]
if __name__ == "__main__":
pytest.main(['-v', '-s'])tests/subdirectory/conftest.py
1
2
3
4
5import pytest
def mid(order):
order.append("mid subpackage")tests/subdirectory/test_module2.py
1
2
3
4
5
6
7
8import pytest
def innermost(order, mid):
order.append("innermost subpackage")
def test_order(order, top):
assert order == ["mid subpackage", "innermost subpackage", "top"]以
tests/test_module1.py
为例:执行test_order()
测试时,首先调用order
(定义在tests/conftest.py
中),返回值为[]
,接着调用top
,top 依次调用order
和innermost
,但需要注意的是innermost
具体是哪一个,因为在子目录的测试文件中也有一个innermost
,这里的 top 调用的是tests/test_module1.py
里的innermost
,返回值为[innermost top]
, 再加上 top 的返回值,最终结果为["innermost top", "top"]
,可以通过测试还有一点需要注意:
tests/subdirectory/test_module2.py
可以直接调用order
和top
而无需显式导入tests/conftest.py
,当调用top
时,innermost
则为定义在子目录下的innermost
但反过来,在
tests/test_module1.py
中调用tests/subdirectory/conftest.py
中的mid
就不可行了
Fixture instantiation order
当执行测试时,确定 fixture 实例化的顺序主要考虑以下3个因素:
作用域(scope): fixture 的作用域是影响实例化顺序的重要因素。不同作用域的 fixture 会在不同阶段实例化,例如,函数级别(function)、模块级别(module)、类级别(class)、以及整个会话级别(session)。
依赖关系(dependencies): fixture 之间的依赖关系也会影响它们的实例化顺序。如果一个 fixture 依赖于另一个 fixture,那么被依赖的 fixture 会在依赖它的 fixture 之前被实例化。
自动使用(autouse): 如果一个 fixture 被设置为自动使用,它会在其作用域内的所有测试之前被实例化。
Higher-scoped fixtures are executed first
有更高级作用域的
fixture
将会被先执行,一般的顺序为session --> package --> module --> class --> function
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
32
33
34
35
36import pytest
def order():
return []
def func(order):
order.append("function")
def cls(order):
order.append("class")
def mod(order):
order.append("module")
def pack(order):
order.append("package")
def sess(order):
order.append("session")
class TestClass:
def test_order(self, func, cls, mod, pack, sess, order):
assert order == ["session", "package", "module", "class", "function"]
Fixtures of the same order execute based on dependencies
当一个
fixture
依赖另一个fixture
,被依赖的 fixture 首先执行假定有如下依赖关系:
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
32
33
34
def order():
return []
def a(order):
order.append("a")
def b(a, order):
order.append("b")
def c(b, order):
order.append("c")
def d(c, b, order):
order.append("d")
def e(d, b, order):
order.append("e")
def f(e, order):
order.append("f")
def g(f, c, order):
order.append("g")
def test_order(g, order):
assert order == ["a", "b", "c", "d", "e", "f", "g"]具体来说:
- g fixture 依赖于 f 和 c。
- f fixture 依赖于 e。
- e fixture 依赖于 d 和 b。
- d fixture 依赖于 c 和 b。
- c fixture 依赖于 b。
- b fixture 依赖于 a。
- a fixture 依赖于 order。
fixture 的执行顺序为:
- order fixture 执行,返回一个空列表。
- a fixture 执行,将字符串 “a” 添加到 order 列表。
- b fixture 执行,将字符串 “b” 添加到 order 列表。
- c fixture 执行,将字符串 “c” 添加到 order 列表。
- d fixture 执行,将字符串 “d” 添加到 order 列表。
- e fixture 执行,将字符串 “e” 添加到 order 列表。
- f fixture 执行,将字符串 “f” 添加到 order 列表。
- g fixture 执行,将字符串 “g” 添加到 order 列表。
在上述代码中,每个 fixture 只被执行一次。这是因为默认情况下,pytest 的 fixture 的作用域是函数级别(function scope),每个测试函数调用时,相关的 fixture 会被执行一次。
关于多次调用同一个 fixture 的补充说明:
这一点可参考官方文档:Fixtures can also be requested more than once during the same test, and pytest won’t execute them again for that test. This means we can request fixtures in multiple fixtures that are dependent on them (and even again in the test itself) without those fixtures being executed more than once.
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
32
33# contents of test_append.py
import pytest
# Arrange
def first_entry():
return "a"
# Arrange
def order():
return []
# Act
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(append_first, order, first_entry):
# Assert
# If a requested fixture was executed once for every time it was requested during a test
# then this test would fail because both append_first and test_string_only would see order as an empty list (i.e. [])
# but since the return value of order was cached (along with any side effects executing it may have had) after the first time it was called,
# both the test and append_first were referencing the same object
# and the test saw the effect append_first had on that object.
assert order == [first_entry]
if __name__ == "__main__":
pytest.main(['-v', '-s'])
Autouse fixtures are executed first within their scope
如果 fixture A 是自动使用的,而 fixture B 不是,但 fixture A 请求了 fixture B,那么在实际应用到 fixture A 的测试中,fixture B 也会被有效地当作自动使用的 fixture。这意味着 fixture B 在这些测试中会在其他非自动使用的 fixtures 之前执行。
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50# 可以自己运行看看实际的 fixture 函数的 实例化顺序
import pytest
def order():
print("order")
return []
def a(order):
print("a")
order.append("a")
def b(a, order):
print("b")
order.append("b")
def c(b, order):
print("c")
order.append("c")
def d(b, order):
print("d")
order.append("d")
def e(d, order):
print("e")
order.append("e")
def f(e, order):
print("f")
order.append("f")
def g(f, c, order):
print("g")
order.append("g")
def test_order_and_g(g, order):
print("test_order_and_g")
assert order == ["a", "b", "c", "d", "e", "f", "g"]
if __name__ == "__main__":
pytest.main(['-v', '-s'])在同一个作用域下,每一个 autouse 的 fixture 会在每个测试前自动执行,即使你并没有显式地请求这个 fixture。
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
32
33
34
35import pytest
def order():
print("order")
return []
def c1(order):
print("c1 autouse")
order.append("c1")
def c2(order):
print("c2")
order.append("c2")
def c3(order, c1):
print("c3")
order.append("c3")
class TestClassWithC1Request:
def test_order(self, order, c1, c3):
print("test_order c1")
assert order == ["c1", "c3"]
class TestClassWithoutC1Request:
def test_order(self, order, c2):
print("test_order c2")
assert order == ["c1", "c2"]
if __name__ == "__main__":
pytest.main(['-v', '-s'])即使一个自动使用的 fixture 请求了一个非自动使用的 fixture,这个非自动使用的 fixture 只会在请求它的那个自动使用 fixture 的上下文中被有效地当作自动使用 fixture。这并不会使得非自动使用的 fixture 在所有可能的情境中都变成自动使用的。
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
32
33
34
35
36
37
38
39
40
41
42import pytest
def order():
print("order")
return []
def c1(order):
print("c1")
order.append("c1")
def c2(order):
print("c2")
order.append("c2")
class TestClassWithAutouse:
def c3(self, order, c2):
print("c3")
order.append("c3")
def test_req(self, order, c1):
print("test_req")
assert order == ["c2", "c3", "c1"]
def test_no_req(self, order):
print("test_no_req")
assert order == ["c2", "c3"]
class TestClassWithoutAutouse:
def test_req(self, order, c1):
print("test_req")
assert order == ["c1"]
def test_no_req(self, order):
print("test_no_req")
assert order == []
if __name__ == "__main__":
pytest.main(['-v', '-s'])
yield fixtures (recommended)
yield 替代 return
在使用 yield 定义 fixture 时,它不再使用 return 返回值,而是使用 yield
yield 的作用是在测试执行之前运行一些代码,并将一个对象传递回请求该 fixture 或测试的地方。
拆分 yield 前后的代码
yield 语句之前的代码在测试执行之前运行,用于设置或准备测试环境
yield 语句之后的代码在测试执行完成后运行,用于清理或执行拆卸操作
Fixture 执行顺序
Pytest 会确定 fixtures 的线性顺序,然后依次执行每个 fixture 直到它返回或使用 yield
一旦测试完成,Pytest 将按照相反的顺序返回到 fixtures 列表,对每个使用了 yield 的 fixture 运行在 yield 语句之后的代码
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# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
def mail_admin():
return MailAdminClient()
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox除了使用 yield 之外,还可以使用 “finalizer” 函数
相对于使用 yield 的 fixtures,另一种选择是直接将 “finalizer” 函数添加到测试的请求上下文对象中
这种方法达到的结果与使用 yield 的 fixtures 类似,但相对来说需要更多的冗余代码
为了使用这种方法,我们需要在需要添加清理代码的 fixture 中请求测试的请求上下文对象,就像请求其他 fixtures 一样。
在获取到 request 上下文对象后,可以通过调用其 addfinalizer 方法,将一个包含清理代码的可调用对象传递给它。
这个清理代码会在测试执行完成后运行,类似于 yield 的后续代码。
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
32
33
34
35
36
37
38
39# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
def mail_admin():
return MailAdminClient()
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
def receiving_user(mail_admin, request):
user = mail_admin.create_user()
def delete_user():
mail_admin.delete_user(user)
request.addfinalizer(delete_user)
return user
def email(sending_user, receiving_user, request):
_email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(_email, receiving_user)
def empty_mailbox():
receiving_user.clear_mailbox()
request.addfinalizer(empty_mailbox)
return _email
def test_email_received(receiving_user, email):
assert email in receiving_user.inbox
Mark
使用 pytest.mark 辅助工具设置测试函数的元数据(metadata)
pytest.mark 辅助工具:
- 使用 pytest.mark 辅助工具,可以在测试函数上设置元数据。元数据可以是标记、条件等,用于影响测试运行的行为。
内置标记:
usefixtures:在测试函数或类上使用 fixtures。
filterwarnings:过滤特定的警告。
skip:始终跳过测试函数。
skipif:如果满足特定条件,则跳过测试函数。
xfail:如果满足特定条件,则产生“预期失败”的结果。
parametrize:对同一个测试函数使用不同的参数执行多次调用。
获取所有标记的方法:
- 可以使用 pytest –markers 命令行选项列出所有标记,包括内置标记和自定义标记。
自定义标记和应用标记的范围:
- 可以创建自定义标记,并将标记应用于整个测试类或模块。
- 这些标记可以被插件使用,也常用于使用 -m 选项在命令行上选择特定的测试。
Note: mark 只能被用于测试,对 fixture 并没有影响
Registering marks
可以在
pytest.ini
文件中设置 markers1
2
3
4[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
serial或者在
pyproject.toml
文件中设置 marker1
2
3
4
5[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"serial",
]
Raising errors on unknown marks
当使用
@pytest.mark.name_of_the_mark
去应用一个pytest.ini
中未定义的 mark,pytest将会发出警告如果还在
pytest.ini
中声明--strict-marker
,那么使用未定义的 mark 将会报告错误,而不是警告1
2
3
4
5[pytest]
addopts = --strict-markers
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
serial
parametrize
在 Pytest 中,当将参数值传递给测试函数时,它们是原样传递的,没有进行任何复制。这意味着如果将一个列表或字典作为参数值传递给测试函数,并且测试函数中对该列表或字典进行了修改,这些修改会在后续的测试用例调用中反映出来。
example
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# content of test_example.py
import copy
import pytest
# 参数化测试函数,接受一个列表作为参数
def test_modify_list(my_list):
# 在测试函数中对传递进来的列表进行修改
my_list.append(4)
assert len(my_list) == 4
###########################################################
# 更推荐
###########################################################
def test_modify_list_deep_copy(my_list):
# 在测试函数中对传递进来的列表进行深复制
my_list_copy = copy.deepcopy(my_list)
my_list_copy.append(4)
assert len(my_list_copy) == 4
# 参数化测试函数,接受一个字典作为参数
def test_modify_dict(my_dict):
# 在测试函数中对传递进来的字典进行修改
my_dict["new_key"] = "new_value"
assert "new_key" in my_dict
pytest-generate_tests
这是 pytest 内置的钩子函数,该钩子在收集测试函数时被调用。通过传递的
metafunc
对象,可以检查请求测试上下文,最重要的是,可以调用metafunc.parametrize()
来实现参数化。metafunc 是 Pytest 中的一个内置对象,它是 Metafunc 类的实例。Metafunc 类提供了一些方法,允许在测试收集阶段动态生成和配置测试函数。下面是一些 metafunc 常用的方法:
fixturenames:
返回测试函数中声明的所有 fixture 的名称的列表function:
返回当前测试函数的 Function 对象config:
返回 Pytest 配置的 Config 对象parametrize:
允许在 pytest_generate_tests 钩子中调用,用于动态生成参数化的测试
pytest_addoption
pytest_addoption
是 Pytest 中的一个内置函数。它是一个钩子函数,用于在 Pytest 运行时处理命令行选项。用户可以在测试项目中的conftest.py
文件中定义这个函数,用于添加自定义的命令行选项。example:
1
2
3
4
5
6
7
8
9
10
11
12# content of conftest.py
def pytest_addoption(parser):
parser.addoption(
"--stringinput",
action="append",
default=[],
help="list of stringinputs to pass to test functions",
)
def pytest_generate_tests(metafunc):
if "stringinput" in metafunc.fixturenames:
metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))1
2
3
4
5# content of test_strings.py
def test_valid_string(stringinput):
assert stringinput.isalpha()
# 执行:pytest --stringinput="hello" --stringinput="world" test_hook.py这里对
addoption
函数的action
参数进行补充说明:store
:将选项的值存储在一个单独的变量中。如果同一个选项多次出现,后面的值会覆盖前面的值。1
parser.addoption("--myoption", action="store", default="default_value", help="My custom option")
store_const
:将选项的值存储为一个常量。通常与 const 参数一起使用。1
parser.addoption("--myoption", action="store_const", const="constant_value", help="My custom option")
store_true
和store_false
:用于处理布尔选项,分别表示 True 和 False。1
2parser.addoption("--enable-feature", action="store_true", help="Enable a feature")
parser.addoption("--disable-feature", action="store_false", help="Disable a feature")append
:如果同一个选项在命令行中出现多次,将其值添加到一个列表中。1
parser.addoption("--stringinput", action="append", default=[], help="List of stringinputs")
count
:记录选项在命令行中出现的次数,用于计数。1
parser.addoption("--verbose", action="count", default=0, help="Increase verbosity level")
callback
:允许指定一个回调函数来处理选项的值。1
2
3
4def callback_function(option, opt_str, value, parser):
# Custom logic to handle the option's value
pass
parser.addoption("--custom-option", action="callback", callback=callback_function, help="Custom option")