pytest和数据库交互的场景设计与实现

263 阅读4分钟

在测试框架中,与数据库的交互是一种常见需求,特别是在接口测试中,可能需要验证接口操作是否正确反映到数据库中,或者从数据库中查询数据,作为接口请求的参数。

以下是一个结合 pytest 测试框架、MySQL 数据库(兼容 OB-MySQL)、以及之前的 YML 用例测试框架 的完整解决方案。


1. 需求分析

1.1 数据库交互场景

  • 前置数据准备
    • 在接口测试之前,需要向数据库插入测试数据。
  • 接口参数化
    • 从数据库中查询特定字段值,动态注入到接口请求参数中。
  • 后置验证
    • 测试接口调用后,验证数据库中的数据是否符合期望。

1.2 设计目标

  • 集成 MySQL 数据库(兼容 OB-MySQL)。
  • 支持从数据库读取字段作为接口参数。
  • 支持在用例中定义数据库操作(如查询语句、验证规则等)。
  • 保持与之前的 YML 用例框架兼容。

2. 数据库交互设计

使用 PyMySQL 库与 MySQL 数据库进行交互。

2.1 数据库工具类

实现一个数据库操作工具类,用于执行 SQL 查询、插入数据和验证数据。

import pymysql

class MySQLHandler:
    def __init__(self, host, port, user, password, database):
        self.connection = pymysql.connect(
            host=host,
            port=port,
            user=user,
            password=password,
            database=database,
            cursorclass=pymysql.cursors.DictCursor
        )

    def execute_query(self, query, params=None):
        """
        执行查询语句
        :param query: SQL 查询语句
        :param params: 查询参数
        :return: 查询结果
        """
        with self.connection.cursor() as cursor:
            cursor.execute(query, params)
            result = cursor.fetchall()
        return result

    def execute_update(self, query, params=None):
        """
        执行插入或更新语句
        :param query: SQL 更新语句
        :param params: 更新参数
        """
        with self.connection.cursor() as cursor:
            cursor.execute(query, params)
        self.connection.commit()

    def close(self):
        """关闭数据库连接"""
        self.connection.close()

3. YML 用例扩展

在 YML 用例中增加对数据库操作的支持,包括:

  1. 动态参数注入:通过数据库查询结果动态填充接口请求参数。
  2. 数据库验证:通过 SQL 查询验证接口调用后的数据状态。

3.1 用例格式示例

扩展后的 YML 用例示例:

test_cases:
  - name: "用户登录"
    request:
      method: POST
      url: "http://example.com/api/login"
      headers:
        Content-Type: "application/json"
      body:
        username: "$db.query.username"  # 从数据库查询结果动态注入
        password: "test_password"
    expected:
      status_code: 200
      body:
        token: "$response.data.token"
    save:
      token: "data.token"
    db_verify:
      - query: "SELECT * FROM users WHERE username = %s"
        params: ["$db.query.username"]
        expected:
          - field: "status"
            value: "active"

  - name: "获取用户信息"
    request:
      method: GET
      url: "http://example.com/api/users/$global.user_id"
      headers:
        Authorization: "Bearer $global.token"
    expected:
      status_code: 200
      body:
        user_id: "$global.user_id"
        username: "$db.query.username"

3.2 关键字段说明

  1. $db.query.field
    • 从数据库查询结果中动态注入字段值。
  2. db_verify
    • 用于指定接口后的数据库验证规则,包括查询语句、参数和期望结果。

4. 测试框架集成

将数据库交互功能集成到之前的 pytest 测试框架中。

4.1 集成代码示例

完整测试框架代码

import pytest
import requests
import yaml
from pymysql import MySQLError
from MySQLHandler import MySQLHandler  # 导入数据库工具类

# 全局上下文
context = {
    "request": {},
    "response": {},
    "global": {},
    "db": {}
}

# 初始化数据库连接
db_handler = MySQLHandler(
    host="localhost",
    port=3306,
    user="root",
    password="password",
    database="test_db"
)

# 加载测试用例
def load_test_cases(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        return yaml.safe_load(f)["test_cases"]

# 保存动态数据到上下文
def save_to_context(response, context, save_mapping):
    for key, path in save_mapping.items():
        value = get_nested_value(response, path.split('.'))
        if value is not None:
            context["global"][key] = value

# 解析占位符
def resolve_placeholders(data, context):
    if isinstance(data, str):
        matches = re.findall(r"\$([\w\.]+)", data)
        for match in matches:
            # 支持从上下文和数据库中获取值
            if match.startswith("db.query"):
                keys = match.split('.')
                value = context["db"].get(keys[-1])
            else:
                keys = match.split('.')
                value = get_nested_value(context, keys)
            if value is not None:
                data = data.replace(f"${{{match}}}", str(value))
        return data
    elif isinstance(data, dict):
        return {k: resolve_placeholders(v, context) for k, v in data.items()}
    elif isinstance(data, list):
        return [resolve_placeholders(item, context) for item in data]
    else:
        return data

# 数据库验证
def verify_database_rules(db_rules, context):
    for rule in db_rules:
        query = resolve_placeholders(rule["query"], context)
        params = resolve_placeholders(rule.get("params", []), context)
        expected_results = rule["expected"]

        # 执行数据库查询
        try:
            results = db_handler.execute_query(query, params)
            if not results:
                raise AssertionError(f"Database query returned no results: {query}")
            
            # 验证字段值
            for expected in expected_results:
                field = expected["field"]
                expected_value = expected["value"]
                actual_value = results[0].get(field)
                assert actual_value == expected_value, f"Expected {field}={expected_value}, but got {actual_value}"
        except MySQLError as e:
            raise AssertionError(f"Database query failed: {e}")

# 运行测试用例
@pytest.mark.parametrize("case", load_test_cases("test_cases.yml"))
def test_api(case):
    # 1. 解析请求参数
    request_data = resolve_placeholders(case["request"], context)

    # 2. 发送请求
    response = requests.request(
        method=request_data["method"],
        url=request_data["url"],
        headers=request_data.get("headers"),
        json=request_data.get("body")
    )

    # 3. 保存请求和响应数据到上下文
    context["request"][case["name"]] = request_data
    context["response"][case["name"]] = response.json()

    # 4. 保存动态数据
    save_mapping = case.get("save", {})
    save_to_context(response.json(), context, save_mapping)

    # 5. 验证数据库规则
    if "db_verify" in case:
        verify_database_rules(case["db_verify"], context)

    # 6. 验证响应
    expected_data = resolve_placeholders(case["expected"], context)
    assert response.status_code == expected_data["status_code"]
    for key, value in expected_data.get("body", {}).items():
        assert value == get_nested_value(response.json(), key.split('.'))

# 关闭数据库连接
@pytest.fixture(scope="session", autouse=True)
def close_db_connection():
    yield
    db_handler.close()

4.2 测试用例执行

  1. 准备测试用例 YML 文件: 将测试用例保存在 test_cases.yml 文件中。
  2. 运行测试: 使用 pytest 执行测试:
    pytest -v
    

5. 示例说明

5.1 数据库表结构

假设测试的数据库表结构如下:

CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(50) NOT NULL,
    status VARCHAR(20) DEFAULT 'inactive'
);

5.2 测试目标

  1. 插入用户数据。
  2. 调用登录接口。
  3. 验证接口返回的 Token。
  4. 确认数据库中用户状态为 active

6. 总结

通过以上设计,测试框架实现了以下功能:

  1. 数据库交互
    • 支持查询、插入、更新操作。
  2. 参数化注入
    • 从数据库结果动态填充接口参数。
  3. 数据库验证
    • 验证接口调用后的数据库状态。
  4. YML 用例集成
    • 保持用例的可读性和扩展性。

这种设计适合复杂的业务场景,特别是接口测试依赖数据库操作的场景。