pytest测试框架中数据分离以及测试用例参数化

发布时间 2023-09-20 14:39:47作者: 类型安全的心

在进行测试自动化过程中,一个重要的最佳实践就是实现测试脚本和测试数据的分离。本文将涉及2个主题,一个是在pytest中如何实现测试用例脚本数据的分离,测试用例如何读入测试数据;二是在pytest中如何实现测试用例参数化。这两点是有区别的,如下图:

flowchart LR TC[测试脚本] -->|读取外部测试数据| B(测试用例) C[测试脚本] --> |参数| D(参数迭代每个测试用例)

前者处理的是测试用例的数据读取,针对的是单一测试用例;后者是针对具有相同测试逻辑的测试用例,叠加参数,这样形成多个测试用例。

测试数据的读取

使用自定义的函数读取

下面的示例在自定义的utils模块中的 load_json_data 方法来读取外部数据文件, 外部数据文件以json格式存储。使用的 shared_datadir 是一个fixture, 来自pytest插件 pytest-datadir, 用于定义外部数据文件的存放的路径。

from utils import utils

def test_post_json(api, shared_datadir):
    filepath = (shared_datadir / 'example.json')
    resp = api.post("/post", headers={"Content-Type": "application/json"}, json=utils.load_json_data(filepath))
    resp_json = resp.json()
    assert resp.status_code == 200, "Response Code"
    assert len(resp_json['json']['sites']) == 3, "Response JSON Site"
    assert resp_json['json']['sites'][0]['name'] == "菜鸟"

utils.py

def load_json_data(filepath):
    """
    :param filepath: The full file path.
    :return: A JSON Object.
    """
    with open(filepath, mode='r', encoding='utf-8') as f:
        data = json.load(f)
    return data

使用自定义的fixture来处理

下面的示例自定义了一个 load_tc_yaml_data 的 fixture, 同时可以在测试用例上通过装饰器提供测试用例的外部数据的文件名。这样的好处可以把测试数据文件名写在测试用例外面,不需要hardcode在测试用例的逻辑代码中。该方法实现也同样需要pytest的插件pytest-datadir支持测试数据路径的解析。使用数据时和fixture使用一致, 函数名即代表数据的返回。

@pytest.mark.data_file('test_get_with_param_in_url.yml')
def test_httpbin_basic(api, load_tc_yaml_data):
    resp = api.get("/headers")
    host = resp.json()["headers"]["Host"]
    assert resp.status_code == 200, "Response Code"
    assert host == "httpbin.org", "Response Host"

    resp = api.get("/user-agent")
    resp_json = resp.json()
    assert resp.status_code == 200, "Response Code"
    assert resp_json["user-agent"] == "python-requests/2.31.0"

    resp = api.get("/get")
    resp_json = resp.json()
    assert resp.status_code == 200, "Response Code"
    assert resp_json["args"] == {}

    resp = api.get("/get", params=load_tc_yaml_data)
    resp_json = resp.json()
    assert resp.status_code == 200, "Response Code"
    assert resp_json["args"] == {'a': '1', 'b': '2'}

    resp = api.get("/cookies/set?name=value")
    resp_json = resp.json()
    assert resp.status_code == 200, "Response Code"
    assert resp_json["cookies"]["name"] == "value"

conftest.py中定义的fixture, 会读取装饰器data_file的值,然后解析路径,读取文件内容并返回。

@pytest.fixture
def load_tc_yaml_data(shared_datadir, request) -> dict:
    marker = request.node.get_closest_marker('data_file')
    data_path = (shared_datadir / marker.args[0])
    with open(data_path, 'r', encoding='utf-8') as yaml_file:
        data = yaml.safe_load(yaml_file)
    return data

测试用例参数化

使用@pytest.mark.parametrize参数化函数

内置的 pytest.mark.parametrize 装饰器可以对测试函数的参数进行参数化。 以下是测试函数的典型示例,该函数实现检查特定输入是否会产生预期输出:

import pytest

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expected

这里, @parametrize 装饰器定义了三个不同的 (test_input,expected) 元组,形成三个测试用例。

使用@pytest.mark.parametrize参数化类

@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

另外一种写法

pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])

class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

标记单个失败用例

@pytest.mark.parametrize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

堆叠参数(参数进行笛卡尔积)

@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass

这将运行测试,参数设置为 x=0/y=2、x=1/y=2、x=0/y=3 和 x=1/y=3,按照装饰器的顺序耗尽参数 。