调用大模型生成测试用例,格式有excel/xmind/mm/json
# !/usr/bin/env python3# -*- coding: utf-8 -*-"""测试用例生成器 - 智谱AI版基于需求文档自动生成测试用例"""import osimport jsonimport requestsimport refrom pathlib import Pathfrom datetime import datetimefrom typing import Dict, List, Optionalimport argparsefrom openai import OpenAI# ==================== 配置区域 ====================#去智谱AI上申请apikey,放到这里ZHIPU_API_KEY = "" # 请替换为你的实际密钥# !/usr/bin/env python3# -*- coding: utf-8 -*-"""测试用例生成器 - 智谱AI版支持生成 XMind、Excel、.mm 格式测试用例支持多种OCR方案"""import jsonimport refrom pathlib import Pathfrom datetime import datetimefrom typing import Dict, Listimport argparseimport requestsimport base64import timeclass TestCaseGenerator:def __init__(self):"""初始化生成器"""if not ZHIPU_API_KEY or ZHIPU_API_KEY == "你的智谱API密钥":print("❌ 错误:请先在脚本中配置智谱API密钥")print(" 获取地址: https://open.bigmodel.cn/")exit(1)self.api_url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"self.api_key = ZHIPU_API_KEY#模型可以自己去智谱找免费的模型self.model = "glm-4-flash"Path("test-docs").mkdir(exist_ok=True)def ocr_with_zhipu(self, image_path: str) -> str:"""使用智谱AI多模态OCR"""try:with open(image_path, 'rb') as f:image_data = base64.b64encode(f.read()).decode('utf-8')ext = Path(image_path).suffix.lower()mime_type = {'.png': 'image/png','.jpg': 'image/jpeg','.jpeg': 'image/jpeg','.gif': 'image/gif','.webp': 'image/webp','.bmp': 'image/bmp'}.get(ext, 'image/png')headers = {"Content-Type": "application/json","Authorization": f"Bearer {self.api_key}"}data = {"model": self.model,"messages": [{"role": "user","content": [{"type": "image_url","image_url": {"url": f"data:{mime_type};base64,{image_data}"}},{"type": "text","text": "请提取这张图片中的所有文字内容。只输出提取的文字,保持原有格式和顺序,不要添加任何解释。"}]}],"max_tokens": 2000}response = requests.post(self.api_url, headers=headers, json=data, timeout=60)if response.status_code == 200:result = response.json()text = result['choices'][0]['message']['content']return text.strip()else:print(f" ⚠️ 智谱OCR失败:{response.status_code}")return ""except Exception as e:print(f" ⚠️ 智谱OCR异常:{e}")return ""def ocr_with_baidu(self, image_path: str) -> str:"""使用百度OCR免费API"""try:# 百度OCR免费API(不需要token,但有频率限制)with open(image_path, 'rb') as f:image_data = base64.b64encode(f.read()).decode('utf-8')# 使用免费的OCR APIurl = "https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic"# 注意:这个需要百度API密钥,这里用公开的测试接口# 实际使用建议注册百度云免费账号print(f" ⚠️ 百度OCR需要配置API密钥,跳过")return ""except Exception as e:return ""def ocr_with_ocrspace(self, image_path: str) -> str:"""使用OCR.Space免费API(无需注册)"""try:with open(image_path, 'rb') as f:response = requests.post('https://api.ocr.space/parse/image',files={'file': f},data={'apikey': 'helloworld', # 免费试用key'language': 'chs', # 中文'isOverlayRequired': False},timeout=30)if response.status_code == 200:result = response.json()if result.get('IsErroredOnProcessing'):return ""parsed_results = result.get('ParsedResults', [])if parsed_results:text = parsed_results[0].get('ParsedText', '')return text.strip()return ""except Exception as e:print(f" ⚠️ OCR.Space异常:{e}")return ""def extract_text_from_image(self, image_path: str) -> str:"""从图片中提取文字(多种OCR方案)"""print(f" 🔍 OCR识别:{Path(image_path).name}")# 方案1:OCR.Space(免费,无需注册)print(f" 📡 尝试 OCR.Space...")text = self.ocr_with_ocrspace(image_path)if text:print(f" ✓ OCR.Space 识别成功,提取 {len(text)} 字符")return text# 方案2:智谱AI多模态print(f" 📡 尝试 智谱AI多模态...")text = self.ocr_with_zhipu(image_path)if text:print(f" ✓ 智谱AI 识别成功,提取 {len(text)} 字符")return textprint(f" ❌ 所有OCR方案均失败")return ""def read_file(self, file_path: str) -> str:"""读取文件内容(支持文本和图片)"""path = Path(file_path)if not path.exists():raise FileNotFoundError(f"找不到文件:{file_path}")# 图片文件image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']if path.suffix.lower() in image_extensions:return self.extract_text_from_image(str(path))# 文本文件text_extensions = ['.txt', '.md', '.markdown']if path.suffix.lower() in text_extensions:encodings = ['utf-8', 'gbk', 'gb2312', 'latin-1']for encoding in encodings:try:with open(path, 'r', encoding=encoding) as f:content = f.read()if content.strip():return contentexcept:continueprint(f" ⚠️ 无法读取文件:{path.name}")return ""def read_directory(self, dir_path: str) -> str:"""读取目录下所有文件"""target_dir = Path(dir_path)if not target_dir.exists():print(f"❌ 目录不存在:{dir_path}")return ""all_texts = []# 支持的文件格式extensions = ['*.txt', '*.md', '*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.bmp']for ext in extensions:for file_path in sorted(target_dir.glob(ext)):if file_path.suffix.lower() in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']:print(f" 🖼️ 处理图片:{file_path.name}")content = self.read_file(str(file_path))if content:all_texts.append(f"=== {file_path.name} (图片) ===\n{content}\n")else:print(f" 📄 读取:{file_path.name}")content = self.read_file(str(file_path))if content:all_texts.append(f"=== {file_path.name} ===\n{content}\n")if not all_texts:print(f"❌ 目录中没有找到可读取的文件")return "\n".join(all_texts)def generate(self, requirement: str) -> List[Dict]:"""调用智谱AI生成测试用例"""if not requirement.strip():print(" ❌ 需求内容为空")return []prompt = f"""请根据以下需求生成测试用例,只输出JSON数组。需求:{requirement[:8000]}要求:每个用例包含:id, title, module, priority, precondition, steps, expected, tagssteps和expected是字符串数组优先级:P0/P1/P2/P3"""headers = {"Content-Type": "application/json","Authorization": f"Bearer {self.api_key}"}data = {"model": self.model,"messages": [{"role": "system", "content": "你是测试工程师,只输出JSON数组。"},{"role": "user", "content": prompt}],"temperature": 0.3,"max_tokens": 4000}try:print(" 🤖 调用智谱AI生成测试用例...")response = requests.post(self.api_url, headers=headers, json=data, timeout=120)if response.status_code == 429:print(" ❌ API调用频率过高,请稍后再试")print(" 💡 提示:智谱AI免费模型有调用限制,建议等待1分钟")return []if response.status_code != 200:print(f" ❌ API错误:{response.status_code}")return []result = response.json()content = result['choices'][0]['message']['content']json_match = re.search(r'\[\s*\{.*\}\s*\]', content, re.DOTALL)if json_match:return json.loads(json_match.group())print(f" ❌ 解析失败")return []except Exception as e:print(f" ❌ 生成失败:{e}")return []def save_to_excel(self, test_cases: List[Dict], output_path: Path):"""保存为Excel格式"""try:from openpyxl import Workbookfrom openpyxl.styles import Font, Alignment, PatternFillwb = Workbook()ws = wb.activews.title = "测试用例"headers = ["用例ID", "标题", "模块", "优先级", "前置条件", "测试步骤", "预期结果", "标签"]ws.append(headers)header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")header_font = Font(color="FFFFFF", bold=True)for col in range(1, len(headers) + 1):cell = ws.cell(row=1, column=col)cell.fill = header_fillcell.font = header_fontcell.alignment = Alignment(horizontal="center", vertical="center")for tc in test_cases:steps_text = "\n".join([f"{i}. {s}" for i, s in enumerate(tc.get('steps', []), 1)])expected_text = "\n".join([f"{i}. {e}" for i, e in enumerate(tc.get('expected', []), 1)])tags_text = ", ".join(tc.get('tags', []))row = [tc.get('id', ''),tc.get('title', ''),tc.get('module', ''),tc.get('priority', ''),tc.get('precondition', ''),steps_text,expected_text,tags_text]ws.append(row)column_widths = {'A': 15, 'B': 35, 'C': 20, 'D': 10, 'E': 30, 'F': 40, 'G': 40, 'H': 20}for col, width in column_widths.items():ws.column_dimensions[col].width = widthwb.save(output_path)print(f" ✅ Excel文件:{output_path}")except ImportError:print(" ⚠️ 未安装openpyxl,请运行:pip install openpyxl")def save_to_xmind(self, test_cases: List[Dict], output_path: Path):"""保存为XMind格式"""xmind_data = []modules = {}for tc in test_cases:module = tc.get('module', '未分类')if module not in modules:modules[module] = []modules[module].append(tc)for module, cases in modules.items():module_node = {"id": module, "title": module, "children": []}for tc in cases:case_node = {"id": tc.get('id'),"title": f"{tc.get('priority', '')}: {tc.get('title', '')}","children": []}if tc.get('precondition'):case_node["children"].append({"id": f"{tc.get('id')}_pre","title": f"前置条件: {tc.get('precondition')}","children": []})steps = tc.get('steps', [])expected = tc.get('expected', [])for i, step in enumerate(steps, 1):step_node = {"id": f"{tc.get('id')}_step_{i}", "title": f"步骤{i}: {step}", "children": []}if i <= len(expected):step_node["children"].append({"id": f"{tc.get('id')}_exp_{i}","title": f"预期{i}: {expected[i - 1]}","children": []})case_node["children"].append(step_node)if tc.get('tags'):case_node["children"].append({"id": f"{tc.get('id')}_tags","title": f"标签: {', '.join(tc.get('tags'))}","children": []})module_node["children"].append(case_node)xmind_data.append(module_node)with open(output_path, 'w', encoding='utf-8') as f:json.dump(xmind_data, f, ensure_ascii=False, indent=2)print(f" ✅ XMind文件:{output_path}")def save_to_mm(self, test_cases: List[Dict], output_path: Path, project_name: str = "测试用例"):"""保存为.mm格式"""xml_lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<map>']xml_lines.append(f' <node TEXT="{project_name}" STYLE="fork">')for priority in ['P0', 'P1', 'P2', 'P3']:priority_cases = [tc for tc in test_cases if tc.get('priority') == priority]if priority_cases:xml_lines.append(f' <node TEXT="{priority}优先级 ({len(priority_cases)}个)" STYLE="bubble" POSITION="right">')modules = {}for tc in priority_cases:module = tc.get('module', '未分类')if module not in modules:modules[module] = []modules[module].append(tc)for module, cases in modules.items():xml_lines.append(f' <node TEXT="{module}" STYLE="fork">')for tc in cases:xml_lines.append(f' <node TEXT="{tc.get("id")}: {tc.get("title")}" STYLE="fork">')if tc.get('precondition'):xml_lines.append(f' <node TEXT="前置条件: {tc.get("precondition")}" STYLE="fork"/>')steps = tc.get('steps', [])expected = tc.get('expected', [])for i, step in enumerate(steps, 1):xml_lines.append(f' <node TEXT="步骤{i}: {step}" STYLE="fork">')if i <= len(expected):xml_lines.append(f' <node TEXT="预期{i}: {expected[i - 1]}" STYLE="fork"/>')xml_lines.append(f' </node>')if tc.get('tags'):xml_lines.append(f' <node TEXT="标签: {", ".join(tc.get("tags"))}" STYLE="fork"/>')xml_lines.append(f' </node>')xml_lines.append(f' </node>')xml_lines.append(f' </node>')xml_lines.append(' </node>')xml_lines.append('</map>')with open(output_path, 'w', encoding='utf-8') as f:f.write('\n'.join(xml_lines))print(f" ✅ .mm文件:{output_path}")def save(self, test_cases: List[Dict], formats: List[str], project_name: str = "测试用例"):"""保存文件"""timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")for fmt in formats:if fmt == "excel":self.save_to_excel(test_cases, Path(f"test-docs/testcases_{timestamp}.xlsx"))elif fmt == "xmind":self.save_to_xmind(test_cases, Path(f"test-docs/testcases_{timestamp}.xmind"))elif fmt == "mm":self.save_to_mm(test_cases, Path(f"test-docs/testcases_{timestamp}.mm"), project_name)elif fmt == "json":output = Path(f"test-docs/testcases_{timestamp}.json")with open(output, 'w', encoding='utf-8') as f:json.dump(test_cases, f, ensure_ascii=False, indent=2)print(f" ✅ JSON文件:{output}")def print_stats(self, test_cases: List[Dict]):"""打印统计"""if not test_cases:returnpriorities = [tc.get('priority', 'P2') for tc in test_cases]print("\n" + "=" * 50)print("📊 统计报告")print("=" * 50)print(f"总用例数:{len(test_cases)}")print(f"P0:{priorities.count('P0')}个")print(f"P1:{priorities.count('P1')}个")print(f"P2:{priorities.count('P2')}个")print(f"P3:{priorities.count('P3')}个")print("=" * 50)def run(self, input_path: str, is_dir: bool = False, formats: List[str] = None, project_name: str = "测试用例"):"""主流程"""if formats is None:formats = ["json"]print("\n" + "=" * 50)print("🧪 智谱AI测试用例生成器")print("=" * 50)print("\n📖 读取需求文档...")if is_dir:requirement = self.read_directory(input_path)else:requirement = self.read_file(input_path)if not requirement:print("❌ 未读取到有效的需求内容")print("💡 提示:如果图片OCR失败,请尝试以下方案:")print(" 1. 将图片中的文字手动复制到txt文件中")print(" 2. 或稍等1分钟后重试(避免API频率限制)")returnprint(f"✅ 读取成功,长度:{len(requirement)}字符")print("\n🤖 生成测试用例...")test_cases = self.generate(requirement)if not test_cases:print("❌ 生成失败")returnprint(f"✅ 生成 {len(test_cases)} 个用例")print("\n💾 保存文件...")self.save(test_cases, formats, project_name)self.print_stats(test_cases)print("\n✨ 完成!")def main():parser = argparse.ArgumentParser(description='智谱AI测试用例生成器')parser.add_argument('-i', '--input', help='输入文件路径')parser.add_argument('-d', '--dir', help='输入目录路径')parser.add_argument('-f', '--format', nargs='+',choices=['json', 'excel', 'xmind', 'mm'],default=['json'],help='输出格式')parser.add_argument('-n', '--name', default='测试用例', help='项目名称')args = parser.parse_args()if not args.input and not args.dir:parser.error("请指定 -i 或 -d")generator = TestCaseGenerator()if args.dir:generator.run(args.dir, is_dir=True, formats=args.format, project_name=args.name)else:generator.run(args.input, is_dir=False, formats=args.format, project_name=args.name)if __name__ == "__main__":main()
夜雨聆风