# !/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 API url = "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 text print(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 content except: continue print(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, tags steps和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 Workbook from openpyxl.styles import Font, Alignment, PatternFill wb = Workbook() ws = wb.active ws.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_fill cell.font = header_font cell.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 = width wb.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: return priorities = [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频率限制)") return print(f"✅ 读取成功,长度:{len(requirement)}字符") print("\n🤖 生成测试用例...") test_cases = self.generate(requirement) if not test_cases: print("❌ 生成失败") return print(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()