基于AI大模型的结构解析自动生成Mock测试数据策略

基于AI大模型的结构解析自动生成Mock测试数据策略

一、概述

微服务架构中,服务间接口调用的测试数据准备工作占据了开发人员大量时间。传统Mock数据构造依赖人工编写JSON/XML,不仅效率低下,还容易遗漏边界条件和异常场景。更严重的是,当接口定义变更时,存量Mock数据必须同步更新,维护成本极高。

AI大模型具备强大的代码理解和结构化生成能力,可以自动解析接口Schema并生成高质量的Mock测试数据。本文提出一种基于AI大模型的结构解析策略,从OpenAPI/Swagger规范出发,经过Schema解析、Prompt构建、AI生成、数据校验四步,实现Mock测试数据的全自动生成。

二、核心原理

2.1 结构解析策略框架

接口定义(Swagger/Protobuf/Java)
    ↓
Schema解析器(按类型适配)
    ↓
中间表示(IR - Intermediate Representation)
    ↓
Prompt模板引擎
    ↓
AI大模型生成
    ↓
JSON Schema校验 → 数据修正
    ↓
Mock数据集

2.2 Schema解析类型适配

接口协议 解析策略 核心组件
OpenAPI 3.0 JSON Schema解析 + $ref递归 SwaggerParser
gRPC/Protobuf .proto文件Message解析 ProtobufParser
Dubbo/HSF Java反射 + 注解解析 JavaAnnotationParser
GraphQL Schema Introspection + Type解析 GraphQLParser

2.3 AI生成策略

AI大模型生成Mock数据时采用"约束生成"策略:

  • 类型约束:字段类型必须匹配(string/integer/boolean/array/object)
  • 格式约束:满足pattern、minLength、maxLength等格式要求
  • 枚举约束:从enum定义的值中选取
  • 业务约束:通过Prompt注入业务上下文,生成语义合理的数据
  • 边界覆盖:每个字段至少包含一个边界值和一个异常值

三、实战配置

3.1 项目结构

mock-ai-generator/
├── config.yaml
├── main.py
├── parser/
│   ├── __init__.py
│   ├── base_parser.py
│   ├── swagger_parser.py
│   ├── protobuf_parser.py
│   └── ir_schema.py
├── prompt/
│   ├── __init__.py
│   ├── template.py
│   └── builder.py
├── generator/
│   ├── __init__.py
│   ├── ai_client.py
│   └── fallback_generator.py
├── validator/
│   ├── __init__.py
│   └── schema_validator.py
└── output/
    └── .gitkeep

3.2 配置与入口

# config.yaml
ai:
  provider: dashscope
  model: qwen-max
  api_key: ${AI_API_KEY}
  temperature: 0.3
  max_tokens: 4096

services:
  - name: order-service
    type: swagger
    url: http://order-service:8080/v3/api-docs
    business_context: 电商订单系统,包含订单创建、支付、退款流程
  - name: user-service
    type: swagger
    url: http://user-service:8080/v3/api-docs
    business_context: 用户中心,包含注册、登录、信息查询

generation:
  data_count: 10
  edge_case_ratio: 0.3
  error_case_ratio: 0.2
  output_format: json
  include_validation_report: true
# main.py
import yaml
import json
import logging
from pathlib import Path
from parser.swagger_parser import SwaggerParser
from prompt.builder import PromptBuilder
from generator.ai_client import AIClient
from generator.fallback_generator import FallbackGenerator
from validator.schema_validator import SchemaValidator

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class MockDataPipeline:
    def __init__(self, config_path: str):
        with open(config_path) as f:
            self.config = yaml.safe_load(f)

        self.ai_client = AIClient(self.config["ai"])
        self.validator = SchemaValidator()
        self.fallback = FallbackGenerator()

    def run(self):
        for svc in self.config["services"]:
            logger.info("处理服务: %s", svc["name"])
            parser = SwaggerParser(svc["url"])
            apis = parser.parse_all()

            for api in apis:
                mock_data = self.process_api(svc, api)
                self.save_output(svc["name"], api, mock_data)

    def process_api(self, svc: dict, api: dict) -> list:
        prompt_builder = PromptBuilder(
            business_context=svc.get("business_context", ""),
            data_count=self.config["generation"]["data_count"],
            edge_case_ratio=self.config["generation"]["edge_case_ratio"],
            error_case_ratio=self.config["generation"]["error_case_ratio"],
        )

        prompt = prompt_builder.build(api)
        logger.info("  API: %s %s", api["method"], api["path"])

        try:
            data = self.ai_client.generate(prompt)
            validated = []
            for item in data:
                result = self.validator.validate(item, api)
                if result["valid"]:
                    validated.append(item)
                else:
                    logger.warn("数据校验失败: %s", result["errors"])
            return validated
        except Exception as e:
            logger.warn("AI生成失败,使用兜底策略: %s", e)
            return self.fallback.generate(api, 5)

    def save_output(self, service_name: str, api: dict, data: list):
        output_dir = Path("output") / service_name
        output_dir.mkdir(parents=True, exist_ok=True)
        path_part = api["path"].replace("/", "_").strip("_")
        filename = f"{api['method'].lower()}_{path_part}.json"
        filepath = output_dir / filename
        with open(filepath, "w") as f:
            json.dump({
                "api": api,
                "mock_data": data,
                "count": len(data),
                "generated_at": str(Path(__file__).stat().st_mtime)
            }, f, ensure_ascii=False, indent=2)
        logger.info("    输出: %s (%d条)", filepath, len(data))

if __name__ == "__main__":
    pipeline = MockDataPipeline("config.yaml")
    pipeline.run()

四、高级实践

4.1 中间表示层设计

# parser/ir_schema.py
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field

@dataclass
class FieldSchema:
    name: str
    field_type: str
    required: bool = False
    description: str = ""
    example: Any = None
    enum_values: List[Any] = field(default_factory=list)
    min_length: Optional[int] = None
    max_length: Optional[int] = None
    minimum: Optional[float] = None
    maximum: Optional[float] = None
    pattern: Optional[str] = None
    properties: List['FieldSchema'] = field(default_factory=list)
    items: Optional['FieldSchema'] = None

@dataclass
class ApiSchema:
    path: str
    method: str
    summary: str
    parameters: List[FieldSchema] = field(default_factory=list)
    request_body: Optional[FieldSchema] = None
    response_body: Optional[FieldSchema] = None

class IRSchemaBuilder:
    @staticmethod
    def from_swagger(path: str, method: str, detail: dict, spec: dict) -> ApiSchema:
        api = ApiSchema(
            path=path,
            method=method.upper(),
            summary=detail.get("summary", "")
        )

        for param in detail.get("parameters", []):
            schema = param.get("schema", {})
            field = FieldSchema(
                name=param["name"],
                field_type=schema.get("type", "string"),
                required=param.get("required", False),
                description=param.get("description", ""),
                example=schema.get("example"),
                enum_values=schema.get("enum", []),
                min_length=schema.get("minLength"),
                max_length=schema.get("maxLength"),
                minimum=schema.get("minimum"),
                maximum=schema.get("maximum"),
                pattern=schema.get("pattern"),
            )
            api.parameters.append(field)

        request_body = detail.get("requestBody", {})
        if request_body:
            content = request_body.get("content", {})
            json_content = content.get("application/json", {})
            body_schema = json_content.get("schema", {})
            api.request_body = IRSchemaBuilder._parse_schema(body_schema, spec)

        responses = detail.get("responses", {})
        success = responses.get("200", responses.get("201", {}))
        resp_content = success.get("content", {})
        resp_json = resp_content.get("application/json", {})
        resp_schema = resp_json.get("schema", {})
        if resp_schema:
            api.response_body = IRSchemaBuilder._parse_schema(resp_schema, spec)

        return api

    @staticmethod
    def _parse_schema(schema: dict, spec: dict) -> Optional[FieldSchema]:
        if not schema:
            return None

        ref = schema.get("$ref", "")
        if ref:
            ref_path = ref.replace("#/", "").split("/")
            resolved = spec
            for key in ref_path:
                resolved = resolved.get(key, {})
            return IRSchemaBuilder._parse_schema(resolved, spec)

        field = FieldSchema(
            name=schema.get("title", "root"),
            field_type=schema.get("type", "object"),
            required=False,
            description=schema.get("description", ""),
            example=schema.get("example"),
            enum_values=schema.get("enum", []),
        )

        if field.field_type == "object":
            for prop_name, prop_schema in schema.get("properties", {}).items():
                child = IRSchemaBuilder._parse_schema(prop_schema, spec)
                if child:
                    child.name = prop_name
                    child.required = prop_name in schema.get("required", [])
                    field.properties.append(child)

        elif field.field_type == "array":
            items = schema.get("items", {})
            field.items = IRSchemaBuilder._parse_schema(items, spec)

        return field

4.2 Prompt模板引擎

# prompt/template.py
from typing import Dict, Any

MOCK_DATA_PROMPT = """你是一个专业的测试数据生成专家。你的任务是严格按照接口定义生成Mock数据。

## 约束规则
1. 数据类型必须严格匹配Schema定义
2. 字符串字段生成有意义的真实数据,不要用"test"或"string"占位
3. 数字字段在合理业务范围内生成,包含边界值
4. 枚举字段从定义中随机选择
5. 时间字段使用ISO 8601格式
6. 嵌套对象必须完整填充

## 生成要求
- 总共生成 {data_count} 组数据
- 其中约 {normal_count} 组为正常数据
- 约 {edge_count} 组为边界数据
- 约 {error_count} 组为异常数据
- 每组数据包含 "_case_type" 字段标记类型

## 接口定义
### {method} {path}
> {summary}

### 请求参数
{parameters_section}

### 请求体Schema
{request_body_section}

### 响应体Schema
{response_body_section}

### 业务上下文
{business_context}

请以JSON数组格式输出,每组的字段必须严格对齐接口定义。
"""

PARAM_TEMPLATE = "| {name} | {param_in} | {field_type} | {required} | {description} | {constraints} |"

SCHEMA_TEMPLATE = """- {name} ({field_type}, {required})
  描述: {description}
  约束: {constraints}
  示例: {example}
"""

def format_constraints(field: dict) -> str:
    parts = []
    if field.get("enum_values"):
        parts.append(f"枚举: {field['enum_values']}")
    if field.get("min_length") is not None:
        parts.append(f"minLen: {field['min_length']}")
    if field.get("max_length") is not None:
        parts.append(f"maxLen: {field['max_length']}")
    if field.get("minimum") is not None:
        parts.append(f"min: {field['minimum']}")
    if field.get("maximum") is not None:
        parts.append(f"max: {field['maximum']}")
    if field.get("pattern"):
        parts.append(f"regex: {field['pattern']}")
    return "; ".join(parts) if parts else "无"

4.3 兜底生成策略

# generator/fallback_generator.py
import random
import string
from datetime import datetime, timedelta
from typing import Dict, List, Any

class FallbackGenerator:
    def generate(self, api: dict, count: int = 5) -> List[Dict]:
        data = []
        for i in range(count):
            record = self._generate_record(api)
            data.append(record)
        return data

    def _generate_record(self, api: dict) -> Dict:
        record = {}
        for param in api.get("parameters", []):
            record[param["name"]] = self._gen_value(param)
        if api.get("request_body"):
            self._fill_object(record, api["request_body"])
        return record

    def _gen_value(self, field: dict):
        field_type = field.get("field_type", "string")
        enum_values = field.get("enum_values", [])
        if enum_values:
            return random.choice(enum_values)

        if field_type == "string":
            if field.get("pattern"):
                return self._gen_by_pattern(field["pattern"])
            min_len = field.get("min_length", 1)
            max_len = field.get("max_length", 20)
            length = random.randint(min_len, max_len)
            return ''.join(random.choices(string.ascii_letters, k=length))

        elif field_type == "integer":
            minimum = field.get("minimum", 0)
            maximum = field.get("maximum", 10000)
            return random.randint(minimum, maximum)

        elif field_type == "number":
            minimum = field.get("minimum", 0.0)
            maximum = field.get("maximum", 10000.0)
            return round(random.uniform(minimum, maximum), 2)

        elif field_type == "boolean":
            return random.choice([True, False])

        elif field_type == "array":
            items = field.get("items", {})
            count = random.randint(0, 3)
            return [self._gen_value(items) for _ in range(count)]

        elif field_type == "object":
            obj = {}
            for prop in field.get("properties", []):
                obj[prop["name"]] = self._gen_value(prop)
            return obj

        return None

    def _fill_object(self, target: Dict, schema: dict):
        for prop in schema.get("properties", []):
            target[prop["name"]] = self._gen_value(prop)

    def _gen_by_pattern(self, pattern: str) -> str:
        if pattern == "^\\d{4}-\\d{2}-\\d{2}$":
            date = datetime.now() - timedelta(days=random.randint(0, 365))
            return date.strftime("%Y-%m-%d")
        if "email" in pattern.lower():
            return f"user{random.randint(1,9999)}@example.com"
        if "phone" in pattern.lower():
            return f"1{random.choice([3,5,7,8,9])}{random.randint(10000000,99999999)}"
        return "generated_" + ''.join(random.choices(string.ascii_lowercase, k=8))

4.4 Schema校验器

# validator/schema_validator.py
import re
from typing import Dict, Any, List, Tuple

class SchemaValidator:
    def validate(self, data: Any, api: dict) -> Dict:
        errors = []
        if api.get("parameters"):
            for param in api["parameters"]:
                if param["name"] in data:
                    error = self._validate_field(
                        data[param["name"]], param
                    )
                    if error:
                        errors.append(f"参数 {param['name']}: {error})
                elif param.get("required"):
                    errors.append(f"参数 {param['name']}: 必填字段缺失")

        if api.get("request_body"):
            body_errors = self._validate_schema(
                data, api["request_body"], ""
            )
            errors.extend(body_errors)

        return {"valid": len(errors) == 0, "errors": errors}

    def _validate_field(self, value: Any, field: dict) -> str:
        expected_type = field.get("field_type", "string")
        actual_type = type(value).__name__

        if expected_type == "integer" and not isinstance(value, int):
            return f"类型错误: 期望{expected_type}, 实际{actual_type}"
        if expected_type == "string" and not isinstance(value, str):
            return f"类型错误: 期望{expected_type}, 实际{actual_type}"
        if expected_type == "number" and not isinstance(value, (int, float)):
            return f"类型错误: 期望{expected_type}, 实际{actual_type}"
        if expected_type == "boolean" and not isinstance(value, bool):
            return f"类型错误: 期望{expected_type}, 实际{actual_type}"

        if field.get("enum_values") and value not in field["enum_values"]:
            return f"枚举值错误: {value} 不在 {field['enum_values']} 中"

        if isinstance(value, str):
            if field.get("min_length") and len(value) < field["min_length"]:
                return f"长度不足: {len(value)} < {field['min_length']}"
            if field.get("max_length") and len(value) > field["max_length"]:
                return f"长度超限: {len(value)} > {field['max_length']}"
            if field.get("pattern") and not re.match(field["pattern"], value):
                return f"正则不匹配: {value} 不匹配 {field['pattern']}"

        if isinstance(value, (int, float)):
            if field.get("minimum") is not None and value < field["minimum"]:
                return f"小于最小值: {value} < {field['minimum']}"
            if field.get("maximum") is not None and value > field["maximum"]:
                return f"大于最大值: {value} > {field['maximum']}"

        return ""

    def _validate_schema(self, data: Any, schema: dict, path: str) -> List[str]:
        errors = []
        if schema.get("field_type") == "object":
            for prop in schema.get("properties", []):
                prop_path = f"{path}.{prop['name']}" if path else prop['name']
                if prop["name"] not in data:
                    if prop.get("required"):
                        errors.append(f"{prop_path}: 必填字段缺失")
                    continue
                error = self._validate_field(data[prop["name"]], prop)
                if error:
                    errors.append(f"{prop_path}: {error}")
                if prop.get("properties"):
                    nested = self._validate_schema(
                        data[prop["name"]], prop, prop_path
                    )
                    errors.extend(nested)

        elif schema.get("field_type") == "array":
            items = schema.get("items", {})
            for idx, item in enumerate(data):
                item_path = f"{path}[{idx}]"
                error = self._validate_field(item, items)
                if error:
                    errors.append(f"{item_path}: {error}")

        return errors

五、最佳实践

实践要点 说明 推荐度
IR中间层 不同接口协议(OpenAPI/Protobuf/GraphQL)统一转为IR再处理 ⭐⭐⭐⭐⭐
温度控制 生成Mock数据时temperature设为0.3,兼顾确定性和多样性 ⭐⭐⭐⭐⭐
兜底策略 AI不可用时使用FallbackGenerator,确保流程不中断 ⭐⭐⭐⭐⭐
Schema校验 AI输出必须经过JSON Schema校验,不合规的自动修正 ⭐⭐⭐⭐
边界覆盖 强制每种数据类型至少包含边界值,如空字符串、最大值、负值 ⭐⭐⭐⭐
增量更新 接口变更时只重新生成变更部分,避免全量覆盖影响已有测试 ⭐⭐⭐

六、总结

基于AI大模型的结构解析自动生成Mock测试数据策略,核心在于三个环节的精密配合:Schema解析将各类接口定义统一为中间表示(IR),Prompt工程将IR转换为大模型能够理解的结构化指令,AI生成与校验确保输出数据的类型正确性和边界覆盖率。

本文的兜底生成策略保证了AI不可用时的流程连续性,Schema校验器则在数据质量层面把住了最后一道关。对于微服务团队,这套方案可以将Mock数据准备时间从小时级压缩到分钟级,显著提升接口联调和自动化测试的效率。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐