1

我正在尝试编写一个测试套件来使用 testinfra 验证某些服务器的状态。

这是我第一次使用 python/testinfra/pytest。

作为一个简短的伪代码示例

测试文件.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 执行所有 server1 的唯一参数,然后将再次启动服务器 2 对服务器 1-3 唯一参数运行。

我找不到一种干净的方法来基本上以 server1 作为主机和 server 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 的理解。

谢谢阿克!我欠你一个:D

4

1 回答 1

1

这是一个很好的问题,看起来你已经从几个不同的角度来解决这个问题了。我自己不是专家,但我可以考虑用几种不同的方式在 pytest 中做这样的事情。它们都涉及将繁重的工作交给固定装置。因此,这里简要介绍了一种调整您已经共享的代码以使用某些固定装置的方法:

默认参数

看起来您有一些不是特定于主机的参数。将它们拉入会话范围的固定装置可能是有意义的,这样您就可以在许多主机上重用相同的值:

@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作为参数添加到任何需要这些值的测试函数。

主机特定参数

您当然可以像以前一样加载这些参数,或者在测试本身中放置一些查找逻辑。这可能是最好和最清晰的方法!另一种选择是有一个参数化的夹具,其值因实例而异:

@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。夹具每次都会根据活动主机返回一个新值。

编写测试

如果我们将繁重的工作交给固定装置,实际测试会变得更简单:

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 文件在本地进行了尝试:

测试/microservices_default_params.yml

files:
    - common_file1.txt
    - common_file2.txt

测试/server1/params/server1_params.yml

files:
    - server1_file.txt

测试/server2/params/server2_params.yml

files:
    - server2_file.txt

测试/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

这似乎至少是您所追求的总体方向。我希望其中一些有用 - 祝你好运,测试愉快!

更新

下面的评论询问如何打破这一点,以便列表中的每个文件生成自己的测试。我不确定最好的方法,但有几个选择可能是:

  • 构建服务器/文件名对的平面列表并将其输入@pytest.mark.parametrize
  • 用于pytest_generate_tests设置动态参数化,类似于此处描述的内容。

无论哪种情况,您都可以从以下内容开始:

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版本打包到一个类中并稍微清理一下逻辑。不过,我们必须从某个地方开始,嗯?

于 2020-07-28T05:38:04.813 回答