ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI生成可执行测试用例:LangChain+Pydantic+Jinja2工程化实践

AI生成可执行测试用例:LangChain+Pydantic+Jinja2工程化实践 简介本资源是一套面向测试工程师与AI工程实践者的AI驱动测试用例生成系统源码聚焦AI测试与测试自动化场景解决传统手工编写测试用例效率低、覆盖不全、难以适配多格式需求的痛点。系统支持从PDF/Word中结构化解析文本、表格含嵌套及图片集成pytesseract与PaddleOCR结合定制提示词与多LLM平台如Qwen、OpenRoute生成多样化用例并可导出为JSON、Excel、XMind等格式配套token统计与用例分布分析功能。压缩包共13个文件含5个核心Python模块testcase_generator.py、document_parser.py、llm_client.py等、前端静态资源HTML/JS/CSS、配置模板.env.example、依赖说明requirements.txt及开发文档TODO.md、README整体仅24KB轻量易部署。目前已有313人学习下载提供开箱即用的完整技术链路——从文档输入、AI生成到结果导出与分析是深入理解LLM在测试领域落地的典型工程实践样本。1. AI生成测试用例技术[源码]不是让AI写完就跑而是把「业务逻辑→可执行断言→环境适配」这条链路真正焊死在CI里你有没有遇到过这样的场景新接口上线测试同学花3小时手写27条覆盖路径的测试用例结果PR合并前发现漏了「空字符串超长token」组合或者回归时发现某条用例因数据库字段名变更而静默失败日志里只报KeyError: user_status但没人知道它本该校验的是user_state——这种“人肉翻译业务需求→代码断言”的过程正在被AI批量击穿。AI生成测试用例技术[源码]核心不是调个大模型API吐几行assert response.status_code 200而是构建一个可追溯、可干预、可嵌入工程流水线的生成闭环输入是Swagger/YAML/代码注释等结构化契约输出是带真实数据构造逻辑、环境感知断言、失败自诊断能力的可执行测试脚本Pytest/Playwright/JUnit且每条用例能反向关联到原始需求ID或代码行。它解决的不是“要不要写测试”而是“怎么让测试用例和代码一样具备版本演进、依赖感知、故障自愈能力”。适合后端API测试、微服务契约验证、以及需要高频迭代的BFF层质量保障团队——尤其当你发现测试用例维护成本已超过开发成本3倍时这个技术栈值得立刻拉源码跑通最小闭环。2. 用LangChainPydantic Schema定义生成器为什么不用纯Prompt Engineering硬刚2.1 为什么必须放弃“你是一个资深测试工程师请生成5条用例”这类Prompt纯Prompt生成的用例有三大硬伤① 断言逻辑脱离实际运行环境比如生成assert len(res[data]) 0但真实接口返回{code:200,data:None}② 无法约束字段类型与边界值生成user_age150却没校验age 120③ 生成结果不可验证你没法用代码自动判断“这条用例是否覆盖了支付超时分支”。我们选择LangChain作为编排框架不是因为它有多酷而是它提供了可插拔的Parser OutputParser Retry机制——这让你能把“生成结果必须符合Pydantic Model”这件事变成一行代码强制约束而不是靠模型玄学。2.2 构建可验证的测试用例Schema从JSON Schema到Pydantic Model先看一个真实可用的用例Schema定义test_case_schema.pyfrom pydantic import BaseModel, Field, validator from typing import List, Optional, Dict, Any class TestCaseStep(BaseModel): step_id: str Field(..., description步骤唯一标识如login_step_1) action: str Field(..., description操作类型http_request / db_query / mock_call) params: Dict[str, Any] Field(..., description参数字典含url/method/body等) validator(params) def validate_http_params(cls, v): if v.get(action) http_request: assert url in v and method in v, HTTP请求必须包含url和method assert v[method].upper() in [GET, POST, PUT, DELETE], 不支持的HTTP方法 return v class TestCase(BaseModel): case_id: str Field(..., description用例ID格式module_api_v1_get_user_001) title: str Field(..., description用例标题需体现业务场景) steps: List[TestCaseStep] Field(..., min_items1, description执行步骤列表) expected_results: Dict[str, Any] Field(..., description期望结果字典键为step_id值为断言表达式) tags: List[str] Field(default_factorylist, description标签smoke/regression/performance) priority: int Field(ge0, le3, default1, description优先级0-高1-中2-低3-跳过) # 生成器强制输出此Schema TEST_CASE_SCHEMA TestCase.schema_json(indent2)提示这个Schema不是摆设。我们在LangChain的PydanticOutputParser中直接传入TestCase类生成器输出会被自动校验——若模型返回{case_id:xxx,steps:[{step_id:1,action:http_request}]}但缺少expected_results会触发重试而非静默接受。这是防止“AI胡说八道”的第一道铁闸。2.3 LangChain链式编排把OpenAPI文档喂给模型的正确姿势关键不是让模型读整个Swagger JSON它会丢失上下文而是做三层切片契约提取层用openapi-spec-validator校验Swagger有效性再用prance解析出单个endpoint的pathscomponents.schemas上下文注入层将当前endpoint的summary、description、requestBody.content.application/json.schema.$ref指向的具体schema拼成一段带缩进的Markdown文本Prompt工程层固定模板prompt_template.txt你是一个测试用例生成专家严格按以下Schema输出JSON {schema} 当前API契约 --- Endpoint: {path} {method} Summary: {summary} Request Schema: {request_schema} Response Schema: {response_schema} Business Rules: {business_rules} # 从Confluence同步的规则库抽取 --- 请生成3条覆盖不同业务路径的测试用例要求 - 每条用例必须包含正向路径、边界值路径、异常路径各1条 - expected_results中每个step_id的断言必须可执行如res.status_code200, res.json()[code]0 - 不得使用虚构字段名所有字段必须来自Request/Response Schemafrom langchain.prompts import PromptTemplate from langchain.llms import Ollama # 本地部署Qwen1.5-7B无网络依赖 from langchain.output_parsers import PydanticOutputParser from langchain.chains import LLMChain llm Ollama(modelqwen:7b, temperature0.3) # 低温度保确定性 parser PydanticOutputParser(pydantic_objectTestCase) prompt PromptTemplate( templateopen(prompt_template.txt).read(), input_variables[schema, path, method, summary, request_schema, response_schema, business_rules], partial_variables{schema: TEST_CASE_SCHEMA} ) chain LLMChain(llmllm, promptprompt, output_parserparser) result chain.run({ path: /api/v1/users, method: POST, summary: 创建用户, request_schema: {...}, # 实际为精简后的UserCreate schema response_schema: {...}, business_rules: 手机号需符合11位数字密码需含大小写字母数字 }) # result 是已通过Pydantic校验的TestCase实例注意这里用Ollama本地部署Qwen而非调用API是因为测试用例生成必须满足离线可审计、响应延迟2s、无token泄露风险三原则。公有云API的rate limit和隐私策略在金融/政企场景下是致命短板。3. 从生成结果到可执行脚本用Jinja2模板引擎完成最后一公里3.1 为什么不能直接用json.dumps()生成测试文件生成的TestCase对象只是中间态它需要被渲染成真实可运行的测试脚本。直接序列化JSON会导致① 缺少import语句② 断言逻辑无法复用项目已有工具类如assert_api_success(res)③ 无法注入环境变量如BASE_URLos.getenv(TEST_ENV)。我们必须用模板引擎做“代码即模板”的终态转换。3.2 Pytest模板设计让生成的用例自带环境感知能力templates/pytest_test.j2# -*- coding: utf-8 -*- Auto-generated test case for {{ case.case_id }} Generated at: {{ now() }} Source API: {{ case.title }} import pytest import os import json from common.api_client import APIClient # 项目自有HTTP客户端 from common.assertions import assert_api_success, assert_field_type # 自定义断言库 pytest.mark.{{ case.tags|join(_) }} class Test{{ case.case_id|replace(., _)|replace(-, _) }}: def setup_method(self): self.client APIClient(base_urlos.getenv(BASE_URL, http://localhost:8000)) {% for step in case.steps %} def test_step_{{ step.step_id }}(self): # Step: {{ step.action }} {% if step.action http_request %} res self.client.{{ step.params.method|lower }}( url{{ step.params.url }}, json{{ step.params.body|tojson|safe }}, headers{{ step.params.headers|default({})|tojson|safe }} ) {% endif %} # Expected result validation {% for key, expr in case.expected_results.items() if key step.step_id %} {{ expr|safe }} {% endfor %} {% endfor %}3.3 渲染执行注入项目上下文并落地为文件from jinja2 import Environment, FileSystemLoader from datetime import datetime def render_test_case(test_case: TestCase, output_dir: str generated_tests): env Environment(loaderFileSystemLoader(templates)) template env.get_template(pytest_test.j2) # 注入项目特有上下文 context { case: test_case, now: lambda: datetime.now().isoformat(), tojson: lambda x: json.dumps(x, ensure_asciiFalse, indent2) } rendered template.render(context) filename f{output_dir}/{test_case.case_id}.py # 确保目录存在 os.makedirs(output_dir, exist_okTrue) with open(filename, w, encodingutf-8) as f: f.write(rendered) print(f✅ Generated: {filename}) return filename # 调用示例 render_test_case(result, output_dirtests/api/generated)提示模板里common.api_client和common.assertions是项目真实存在的模块路径。这意味着生成的用例天然继承项目HTTP重试策略、鉴权头注入、断言失败截图等能力——不是孤立脚本而是工程化测试资产的一部分。4. 避坑生成测试用例时的5个血泪经验4.1 现象生成的用例总在res.json()[data]处报KeyError但Swagger明确写了data字段必填原因模型记住了“常见响应结构”却忽略了当前API的responses.200.schema.properties.data.type实际为null允许为空。OpenAPI解析时未处理nullable: true字段导致生成的断言强行访问[data]。解决在契约提取层增加nullable字段检测对nullable: true字段生成assert data in res.json() and res.json()[data] is not None而非直接访问。4.2 现象生成的边界值用例如age-1被后端校验拦截但用例本身没声明这是“负向用例”原因Prompt里只写了“覆盖边界值”但未定义负向用例的断言范式应校验status_code400age in res.json()[message]。模型默认生成正向断言。解决在TestCaseSchema中增加is_negative: bool False字段并在Prompt中明确“负向用例必须设置is_negativeTrue且expected_results中对应step的断言必须校验错误码与错误信息”。4.3 现象同一API多次生成用例ID重复如user_create_001导致Git冲突原因用例ID生成逻辑简单拼接未结合Git commit hash或时间戳做唯一性保障。解决在TestCase模型中重写case_id生成逻辑validator(case_id, alwaysTrue) def generate_case_id(cls, v, values): if not v: path values.get(steps, [{}])[0].get(params, {}).get(url, unknown) method values.get(steps, [{}])[0].get(params, {}).get(method, GET) timestamp int(time.time() * 1000) % 10000 return f{path.strip(/).replace(/, _)}_{method.lower()}_{timestamp:04d} return v4.4 现象生成的Playwright UI用例里出现page.locator(#nonexistent-button).click()但页面实际没有该元素原因模型从HTML片段中“脑补”了不存在的DOM结构。UI测试必须基于真实DOM快照而非文本描述。解决弃用纯文本Prompt改为用Playwright录制真实操作生成.har文件提取request.urlresponse.body中的CSS选择器仅允许模型从该选择器白名单中选取。4.5 现象CI流水线里执行生成的用例90%失败率但本地运行全绿原因生成时未注入环境变量上下文如BASE_URL指向localhost而CI中BASE_URL为https://staging-api.example.com且该环境有额外鉴权中间件。解决在Jinja2模板中强制使用os.getenv(BASE_URL)并在CI job中预置export BASE_URLhttps://staging-api.example.com——让生成逻辑与执行环境解耦而非在生成时硬编码URL。5. 让生成的用例真正活起来接入CI/CD并建立反馈闭环5.1 在GitLab CI中自动触发生成与验证在.gitlab-ci.yml中新增jobgenerate-test-cases: stage: test image: python:3.10-slim before_script: - pip install -r requirements.txt script: - python generate_cases.py --openapi ./openapi.yaml --output-dir tests/generated/ - pytest tests/generated/ --tbshort -v --maxfail1 # 快速验证生成结果可执行 artifacts: paths: - tests/generated/ allow_failure: false # 生成失败即阻断流水线关键点--maxfail1确保只要一条生成用例语法错误或导入失败立即终止避免污染测试集。5.2 建立“生成-执行-反馈”数据飞轮光生成不够要让AI从失败中学习。我们在测试报告后追加反馈收集# post_test_feedback.py import json import subprocess from pathlib import Path def collect_failure_feedback(): # 执行生成的用例捕获失败详情 result subprocess.run( [pytest, tests/generated/, --tbno, -q, --json-report], capture_outputTrue, textTrue ) if result.returncode ! 0: try: report json.loads(result.stdout) for test in report.get(tests, []): if test.get(outcome) failed: # 提取失败原因SyntaxError / KeyError / AssertionError error_type test[call][exception][type] error_msg test[call][exception][message] # 写入反馈日志供后续微调模型 feedback_log { case_id: test[nodeid].split(::)[-1].replace(.py, ), error_type: error_type, error_message: error_msg, generated_at: datetime.now().isoformat() } with open(feedback/failure_log.jsonl, a) as f: f.write(json.dumps(feedback_log, ensure_asciiFalse) \n) except Exception as e: print(fFailed to parse pytest report: {e}) if __name__ __main__: collect_failure_feedback()这个failure_log.jsonl就是我们的“AI训练燃料”。当累计100条KeyError: user_status时我们就能定位到Swagger解析环节漏掉了字段别名映射——然后更新契约提取器而非反复调低模型temperature。5.3 用Diff比对实现用例变更可追溯每次生成新用例都和上一版做diff并存档# 生成时自动存档 cp -r tests/generated/ tests/generated_$(date %Y%m%d_%H%M%S)/ # 比对差异仅显示新增/删除的用例文件 diff -rq tests/generated_20240520_100000/ tests/generated_20240520_110000/ | \ grep -E (Only in|differ) | \ sed s/Only in //; s/:.*//; s/ differ$// | \ sort | uniq tests/generated_diff.log这样当某次发布后线上出现500错误你可以立刻查generated_diff.log确认是否新增了某条覆盖payment_timeout分支的用例——如果没覆盖说明生成策略漏了该场景立刻修正Prompt如果覆盖了但用例失败说明是代码缺陷而非生成问题。我坚持把生成器输出目录设为tests/generated/而非tests/是因为真正的工程化不是让AI替代人而是让人能一眼看清“哪些是机器写的哪些是人写的哪些是机器改过的”。每次Code Review时我都会打开git diff tests/generated/像审代码一样审AI的产出——它错了我改Prompt它对了我把它merge进主干。这套机制跑满3个月后我们API测试用例覆盖率从62%升到89%而测试同学每周手动编写用例的时间下降了73%。希望帮到你。本文还有配套的精品资源点击获取
返回列表