使用 testinfra_hosts 的数组,您可以控制测试中每个主机使用的参数化值吗?
Using an array of testinfra_hosts, can you control the parametrized values used for each host in a test?
我正在尝试编写一个测试套件来使用 testinfra 验证某些服务器的状态。
这是我第一次使用 python/testinfra/pytest。
作为一个简短的伪代码示例
test_files.py
testinfra_hosts=[server1,server2,server3]
with open("tests/microservices_default_params.yml", "r") as f:
try:
default_params = yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
with open("tests/" + server + "/params/" + server + "_params.yml", "r") as f:
try:
instance_params = yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
@pytest.mark.parametrize(
"name", [] + default_params["files"] + instance_params["files"]
)
def test_files(host, name):
file = host.file(name)
assert file.exists
每个服务器都有自己独特的 params yaml 文件。
我希望每台服务器都经过相同的测试,但是我需要每台服务器 运行 使用其各自 .yml 文件中自己的参数化值进行测试。
上面代码的问题在于,它会尝试对服务器 2 和 3 执行服务器 1 的所有唯一参数,然后服务器 2 将再次启动 运行 针对服务器 1-3 唯一参数。
我找不到一种干净的方法来基本上进行测试 运行 一次以 server1 作为主机,服务器 1 参数,然后再次对 server2 和 server2 参数等进行相同的测试
我已经尝试在测试文件本身中使用 for 循环,将每个 instance_params.yml 读入一个字典,键是服务器名称和包含所有服务器参数的值 - 但那没有味道非常好,因为断言在循环内,如果该服务器的参数之一失败,则循环退出并且不会尝试对该服务器进行任何进一步的参数。
我已经研究过 pytest_collection_modifyitems,但我不太清楚如何让它做我想做的事。我觉得可能有一个我缺少的简单解决方案。
我的最后一招是将测试和参数化参数单独分开,如
@pytest.mark.parametrize(
"server1_params", instance_params['server1']['files]
)
def_test_files_server1(host, server1_params):
...
@pytest.mark.parametrize(
"server2_params", instance_params['server2']['files]
)
def_test_files_server2(host,server2_params):
...
虽然我觉得这种方法不对。
任何对新手的帮助都将不胜感激,我以前从未在这里问过任何问题
希望它有意义:)
更新:
@ajk 找到解决方案了!
pytest_generate_tests 函数正是我所需要的——而且我在这个过程中进一步加深了对 pytest 的理解。
谢谢 ajk!我欠你一个 :D
这是一个很好的问题,看起来您已经从几个不同的角度提出了这个问题。我自己不是专家,但我可以想出几种不同的方法在 pytest 中做这样的事情。它们都涉及将繁重的工作移交给 fixtures。因此,这里有一个 一种 方法的快速细分,可以调整您已经共享的代码以使用一些固定装置:
默认参数
看起来您有一些参数不是主机特定的。将它们拉入 session-scoped fixture 可能是有意义的,这样您就可以在许多主机上重复使用相同的值:
@pytest.fixture(scope="session")
def default_params():
with open("tests/microservices_default_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
这样,您可以将 default_params
作为参数添加到任何需要这些值的测试函数。
主机特定参数
您当然可以像以前一样加载这些参数,或者在测试本身中放置一些查找逻辑。这可能是最好和最清晰的方法!另一种选择是使用 parametrized fixture 其值因实例而异:
@pytest.fixture(scope="function", params=testinfra_hosts)
def instance_params(request):
with open(f"tests/{request.param}/params/{request.param}_params.yml", "r") as f:
try:
return request.param, yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
现在,如果我们将 instance_params
作为参数添加到测试函数,它将 运行 对 testinfra_hosts
中的每个条目进行一次测试。根据活动主机,灯具每次都会 return 一个新值。
编写测试
如果我们将繁重的工作交给 fixtures,实际测试会变得更简单:
def test_files(default_params, instance_params):
hostname, params = instance_params
merged_files = default_params["files"] + params["files"]
print(f"""
Host: {hostname}
Files: {merged_files}
""")
(我什至没有在这里断言任何东西,只是玩弄固定装置以确保他们正在做我认为他们正在做的事情)
兜风
我使用以下示例 yaml 文件在本地进行了尝试:
tests/microservices_default_params.yml
files:
- common_file1.txt
- common_file2.txt
tests/server1/params/server1_params.yml
files:
- server1_file.txt
tests/server2/params/server2_params.yml
files:
- server2_file.txt
tests/server3/params/server3_params.yml
files:
- server3_file.txt
- server3_file2.txt
运行 测试文件产生:
test_infra.py::test_files[server1]
Host: server1
Files: ['common_file1.txt', 'common_file2.txt', 'server1_file.txt']
PASSED
test_infra.py::test_files[server2]
Host: server2
Files: ['common_file1.txt', 'common_file2.txt', 'server2_file.txt']
PASSED
test_infra.py::test_files[server3]
Host: server3
Files: ['common_file1.txt', 'common_file2.txt', 'server3_file.txt', 'server3_file2.txt']
PASSED
这似乎至少是您所追求的大体方向。我希望其中一些有用 - 祝你好运,测试愉快!
更新
下面的评论询问是否将其分解,以便列表中的每个文件都生成自己的测试。我不确定这样做的最佳方法,但有几个选项可能是:
- 构建 server/filename 对的平面列表并将其输入
@pytest.mark.parametrize
- 使用
pytest_generate_tests
设置动态参数化,类似于 here 中描述的内容。
无论哪种情况,您都可以从以下内容开始:
import pytest
import yaml
testinfra_hosts = ["server1", "server2", "server3"]
def get_default_params():
with open("tests/microservices_default_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
def get_instance_params(instance):
with open(f"tests/{instance}/params/{instance}_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
要使用 @pytest.mark.parametrize
,您可以继续使用:
def get_instance_files(instances):
default_files = get_default_params()["files"]
for instance in instances:
instance_files = default_files + get_instance_params(instance)["files"]
for filename in instance_files:
yield (instance, filename)
@pytest.mark.parametrize("instance_file", get_instance_files(testinfra_hosts))
def test_files(instance_file):
hostname, filename = instance_file
print(
f"""
Host: {hostname}
Files: {filename}
"""
)
或者要采用 pytest_generate_tests
方法,您可以改为这样做:
def pytest_generate_tests(metafunc):
if "instance_file" in metafunc.fixturenames:
default_files = get_default_params()["files"]
params = [
(host, filename)
for host in testinfra_hosts
for filename in (
default_files + get_instance_params(host)["files"]
)
]
metafunc.parametrize(
"instance_file", params, ids=["_".join(param) for param in params]
)
def test_files(instance_file):
hostname, filename = instance_file
print(
f"""
Host: {hostname}
Files: {filename}
"""
)
任何一种方法都可以,我怀疑更有经验的人可能会将 pytest_generate_tests
版本打包成 class 并清理一下逻辑。不过,我们必须从某个地方开始,嗯?
我正在尝试编写一个测试套件来使用 testinfra 验证某些服务器的状态。
这是我第一次使用 python/testinfra/pytest。
作为一个简短的伪代码示例
test_files.py
testinfra_hosts=[server1,server2,server3]
with open("tests/microservices_default_params.yml", "r") as f:
try:
default_params = yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
with open("tests/" + server + "/params/" + server + "_params.yml", "r") as f:
try:
instance_params = yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
@pytest.mark.parametrize(
"name", [] + default_params["files"] + instance_params["files"]
)
def test_files(host, name):
file = host.file(name)
assert file.exists
每个服务器都有自己独特的 params yaml 文件。 我希望每台服务器都经过相同的测试,但是我需要每台服务器 运行 使用其各自 .yml 文件中自己的参数化值进行测试。
上面代码的问题在于,它会尝试对服务器 2 和 3 执行服务器 1 的所有唯一参数,然后服务器 2 将再次启动 运行 针对服务器 1-3 唯一参数。
我找不到一种干净的方法来基本上进行测试 运行 一次以 server1 作为主机,服务器 1 参数,然后再次对 server2 和 server2 参数等进行相同的测试
我已经尝试在测试文件本身中使用 for 循环,将每个 instance_params.yml 读入一个字典,键是服务器名称和包含所有服务器参数的值 - 但那没有味道非常好,因为断言在循环内,如果该服务器的参数之一失败,则循环退出并且不会尝试对该服务器进行任何进一步的参数。
我已经研究过 pytest_collection_modifyitems,但我不太清楚如何让它做我想做的事。我觉得可能有一个我缺少的简单解决方案。
我的最后一招是将测试和参数化参数单独分开,如
@pytest.mark.parametrize(
"server1_params", instance_params['server1']['files]
)
def_test_files_server1(host, server1_params):
...
@pytest.mark.parametrize(
"server2_params", instance_params['server2']['files]
)
def_test_files_server2(host,server2_params):
...
虽然我觉得这种方法不对。
任何对新手的帮助都将不胜感激,我以前从未在这里问过任何问题 希望它有意义:)
更新: @ajk 找到解决方案了! pytest_generate_tests 函数正是我所需要的——而且我在这个过程中进一步加深了对 pytest 的理解。
谢谢 ajk!我欠你一个 :D
这是一个很好的问题,看起来您已经从几个不同的角度提出了这个问题。我自己不是专家,但我可以想出几种不同的方法在 pytest 中做这样的事情。它们都涉及将繁重的工作移交给 fixtures。因此,这里有一个 一种 方法的快速细分,可以调整您已经共享的代码以使用一些固定装置:
默认参数
看起来您有一些参数不是主机特定的。将它们拉入 session-scoped fixture 可能是有意义的,这样您就可以在许多主机上重复使用相同的值:
@pytest.fixture(scope="session")
def default_params():
with open("tests/microservices_default_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
这样,您可以将 default_params
作为参数添加到任何需要这些值的测试函数。
主机特定参数
您当然可以像以前一样加载这些参数,或者在测试本身中放置一些查找逻辑。这可能是最好和最清晰的方法!另一种选择是使用 parametrized fixture 其值因实例而异:
@pytest.fixture(scope="function", params=testinfra_hosts)
def instance_params(request):
with open(f"tests/{request.param}/params/{request.param}_params.yml", "r") as f:
try:
return request.param, yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
现在,如果我们将 instance_params
作为参数添加到测试函数,它将 运行 对 testinfra_hosts
中的每个条目进行一次测试。根据活动主机,灯具每次都会 return 一个新值。
编写测试
如果我们将繁重的工作交给 fixtures,实际测试会变得更简单:
def test_files(default_params, instance_params):
hostname, params = instance_params
merged_files = default_params["files"] + params["files"]
print(f"""
Host: {hostname}
Files: {merged_files}
""")
(我什至没有在这里断言任何东西,只是玩弄固定装置以确保他们正在做我认为他们正在做的事情)
兜风
我使用以下示例 yaml 文件在本地进行了尝试:
tests/microservices_default_params.yml
files:
- common_file1.txt
- common_file2.txt
tests/server1/params/server1_params.yml
files:
- server1_file.txt
tests/server2/params/server2_params.yml
files:
- server2_file.txt
tests/server3/params/server3_params.yml
files:
- server3_file.txt
- server3_file2.txt
运行 测试文件产生:
test_infra.py::test_files[server1]
Host: server1
Files: ['common_file1.txt', 'common_file2.txt', 'server1_file.txt']
PASSED
test_infra.py::test_files[server2]
Host: server2
Files: ['common_file1.txt', 'common_file2.txt', 'server2_file.txt']
PASSED
test_infra.py::test_files[server3]
Host: server3
Files: ['common_file1.txt', 'common_file2.txt', 'server3_file.txt', 'server3_file2.txt']
PASSED
这似乎至少是您所追求的大体方向。我希望其中一些有用 - 祝你好运,测试愉快!
更新
下面的评论询问是否将其分解,以便列表中的每个文件都生成自己的测试。我不确定这样做的最佳方法,但有几个选项可能是:
- 构建 server/filename 对的平面列表并将其输入
@pytest.mark.parametrize
- 使用
pytest_generate_tests
设置动态参数化,类似于 here 中描述的内容。
无论哪种情况,您都可以从以下内容开始:
import pytest
import yaml
testinfra_hosts = ["server1", "server2", "server3"]
def get_default_params():
with open("tests/microservices_default_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
def get_instance_params(instance):
with open(f"tests/{instance}/params/{instance}_params.yml", "r") as f:
try:
return yaml.safe_load(f)
except yaml.YAMLError as exc:
print(exc)
要使用 @pytest.mark.parametrize
,您可以继续使用:
def get_instance_files(instances):
default_files = get_default_params()["files"]
for instance in instances:
instance_files = default_files + get_instance_params(instance)["files"]
for filename in instance_files:
yield (instance, filename)
@pytest.mark.parametrize("instance_file", get_instance_files(testinfra_hosts))
def test_files(instance_file):
hostname, filename = instance_file
print(
f"""
Host: {hostname}
Files: {filename}
"""
)
或者要采用 pytest_generate_tests
方法,您可以改为这样做:
def pytest_generate_tests(metafunc):
if "instance_file" in metafunc.fixturenames:
default_files = get_default_params()["files"]
params = [
(host, filename)
for host in testinfra_hosts
for filename in (
default_files + get_instance_params(host)["files"]
)
]
metafunc.parametrize(
"instance_file", params, ids=["_".join(param) for param in params]
)
def test_files(instance_file):
hostname, filename = instance_file
print(
f"""
Host: {hostname}
Files: {filename}
"""
)
任何一种方法都可以,我怀疑更有经验的人可能会将 pytest_generate_tests
版本打包成 class 并清理一下逻辑。不过,我们必须从某个地方开始,嗯?