首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >通过Python实现需求转为playwright测试脚本

通过Python实现需求转为playwright测试脚本

作者头像
顾翔
发布2026-04-28 13:31:13
发布2026-04-28 13:31:13
240
举报

结构

本文介绍如何使用Python实现需求转为playwright测试脚本,本文的思路是:

代码语言:javascript
复制

读取需求->生成测试脚本->是够通过—是—>测试结束
       ^     |
        |————否—————

本程序的代码结果如下

代码语言:javascript
复制

|——logs
|     |
|     ———test_run.log(运行后自动生成)
|——outputs
|     |
|     ——— /test_history  (目录、运行后自动生成)
|   test_current.py(运行后生成的测试程序)
|   fix_report.json(运行后自动生成)
|
|——skills
|        |
|———/test-orchestrator
|     |
|——————skill.md: 能力文件
|   /scripts
|   |
|—————————executor.py :运行测试程序
|        fixer.py:修复测试程序
|        generator.py:生成测试程序
|        orchestrator.py:调度者
|——main.py  主程序
|——req.txt  需求文档

实现程序

main.py

主测试程序 python

代码语言:javascript
复制

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
一键启动测试自动化系统
"""
import sys
from pathlib import Path


# 添加脚本路径
sys.path.insert(0, str(Path(__file__).parent / "skills" / "test-orchestrator" / "scripts"))
from orchestrator import TestOrchestrator

def main():
    orchestrator = TestOrchestrator(
        req_file="req.txt",
        max_retry=5,
        headless=True
    )
    
    success = orchestrator.run()
    
    if success:
        print("\n测试自动化完成,所有用例通过!")
        print(f"   最终测试文件: outputs/test_current.py")
    else:
        print("\n达到最大重试次数,请人工介入检查")
        print(f"   修复报告: outputs/fix_report.json")
    
    return 0 if success else 1

if __name__ == "__main__":
    sys.exit(main())

executor.py

测试执行器 python

代码语言:javascript
复制

#!/usr/bin/env python
"""
测试执行器 - 运行 pytest 并解析结果
"""
import subprocess
import json
import re
from pathlib import Path
from typing import Dict, List

class TestExecutor:
    def __init__(self, headless: bool = True):
        self.headless = headless
    
    def run(self, test_file: str) -> Dict:
        """
        执行测试文件,返回结构化结果
        """
        test_path = Path(test_file)
        if not test_path.exists():
            return {
                "success": False,
                "error": f"测试文件不存在: {test_file}",
                "exit_code": -1,
                "passed": [],
                "failed": []
            }
        
        # 构建命令
        cmd = ["pytest", str(test_path), "-v", "--tb=short", "--color=no"]
        
        # 修复:headed 是 flag,不加 =value
        if not self.headless:
            cmd.append("--headed")
        
        # 添加 JSON 报告
        report_file = test_path.parent / ".pytest_report.json"
        cmd.extend(["--json-report", f"--json-report-file={report_file}"])
        
        try:
            import time
            start = time.time()
            
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=120,
                env={**subprocess.os.environ, "PYTHONUNBUFFERED": "1"}
            )
            
            duration = time.time() - start
            
            # 解析结果
            passed = []
            failed = []
            
            for line in result.stdout.split("\n"):
                if "PASSED" in line and "::" in line:
                    match = re.search(r'(\w+)::(\w+)', line)
                    if match:
                        passed.append(match.group(2))
                elif "FAILED" in line and "::" in line:
                    match = re.search(r'(\w+)::(\w+)', line)
                    if match:
                        failed.append(match.group(2))
            
            error_msg = ""
            if report_file.exists():
                try:
                    with open(report_file) as f:
                        report = json.load(f)
                    for test in report.get("tests", []):
                        if test.get("outcome") == "failed":
                            error_msg += test.get("longrepr", "")[:1000]
                except:
                    pass
            
            if not error_msg:
                error_msg = result.stdout + "\n" + result.stderr
            
            return {
                "success": result.returncode == 0,
                "passed": passed,
                "failed": failed,
                "error": error_msg[:2000],
                "exit_code": result.returncode,
                "duration": duration
            }
            
        except subprocess.TimeoutExpired:
            return {
                "success": False,
                "error": "测试执行超时(超过 120 秒)",
                "exit_code": -2,
                "passed": [],
                "failed": []
            }
        except Exception as e:
            return {
                "success": False,
                "error": f"执行异常: {str(e)}",
                "exit_code": -3,
                "passed": [],
                "failed": []
            }

fixer.py

修复测试程序 python

代码语言:javascript
复制

import os
import re
import logging
from typing import Dict, Optional, List, Tuple
from openai import OpenAI
import ast  

logger = logging.getLogger(__name__)

class TestFixer:
    def __init__(self):
        self.client = OpenAI(
            api_key=os.getenv("DASHSCOPE_API_KEY"),
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
        )
        self.model = os.getenv("QWEN_MODEL", "qwen-plus")
        self.max_retries = 3

    def _classify_error(self, error_msg: str, traceback: str = "") -> Dict:
        """精细化错误分类"""
        error_lower = error_msg.lower()
        traceback_lower = traceback.lower()
        
        # Playwright 特有错误
        if "timeout" in error_lower:
            if "expect" in error_lower:
                return {"type": "AssertionTimeout", "severity": "high"}
            return {"type": "NavigationTimeout", "severity": "medium"}
        
        elif "locator" in error_lower and "not found" in error_lower:
            return {"type": "ElementNotFound", "severity": "high"}
        
        elif "not visible" in error_lower or "hidden" in error_lower:
            return {"type": "ElementNotVisible", "severity": "medium"}
        
        elif "assertion" in error_lower or "expected" in error_lower:
            # 提取实际值和期望值
            expected = re.search(r"expected:?\s*['\"](.+?)['\"]", error_msg, re.I)
            actual = re.search(r"actual:?\s*['\"](.+?)['\"]", error_msg, re.I)
            return {
                "type": "AssertionError",
                "severity": "high",
                "expected": expected.group(1) if expected else None,
                "actual": actual.group(1) if actual else None
            }
        
        elif "database" in traceback_lower or "mysql" in traceback_lower:
            return {"type": "DatabaseError", "severity": "critical"}
        
        else:
            return {"type": "UnknownError", "severity": "low"}

    def _extract_code_block(self, content: str) -> Optional[str]:
        """提取代码块,支持多种格式"""
        # 尝试提取 ```python ... ``` 代码块
        # 修复了正则表达式中的引号转义问题
        patterns = [
            r"```python\n(.*?)```",
            r"```\n(.*?)```", 
            "```python\n(.*?)```",
            "```\n(.*?)```"
        ]
        
        for pattern in patterns:
            match = re.search(pattern, content, re.DOTALL)
            if match:
                return match.group(1).strip()
        
        # 如果没有代码块标记,尝试提取Python代码
        if "def test_" in content or "class Test" in content:
            return content.strip()
        
        return None

    def _generate_fix_prompt(self, code: str, error_info: Dict, requirements: str, 
                            html_context: str = "", db_context: str = "") -> str:
        """生成修复提示词"""
        error_detail = error_info.get("error", "无详细信息")
        traceback = error_info.get("traceback", "")
        error_analysis = self._classify_error(error_detail, traceback)
        
        prompt = f"""
            你是一个 Playwright + Pytest 自动化测试专家。请分析错误并修复测试代码。

            ## 错误分析
            - 错误类型: {error_analysis['type']}
            - 严重程度: {error_analysis['severity']}
            - 错误信息: {error_detail}

            ## 原始需求
            {requirements}

            ## 失败的测试代码
            ```python
            {code}
            # 完整错误堆栈
            {traceback or error_detail}
            ```
            """
        # 添加HTML上下文(如果有)
        if html_context:
            truncated_html = html_context[:2000] if len(html_context) > 2000 else html_context
            prompt += f"""
                ## 页面HTML结构(调试信息)
                ```html
                {truncated_html}
                ```
            """
        # 添加数据库上下文(如果有)
        if db_context:
            truncated_db = db_context[:2000] if len(db_context) > 2000 else db_context
            prompt += f"""
            ## 数据库状态
            {truncated_db}
            """
        prompt += """
            ## 修复要求
            1. 标点符号保护:错误消息必须与需求文档完全一致,保留所有标点符号(,。!)
            2. 等待策略:添加合适的等待(wait_for_selector, wait_for_timeout)
            3. 错误处理:添加 try-except 和重试机制
            4. 数据库清理:确保测试数据正确清理
            5. 断言优化:使用 expect() 而不是 assert

            ## 常见问题修复指南
            - 超时错误:增加 timeout 参数,添加等待条件
            - 元素未找到:检查选择器,添加等待,确保页面已加载
            - 断言失败:检查实际值格式(如换行符、空格),使用 to_contain_text 而不是 to_have_text
            - 数据库错误:检查外键约束,确保测试数据完整

            请输出修复后的完整 Python 代码,只返回代码,不要解释。
        """
        return prompt

    def fix(self, code: str, error_info: Dict, requirements: str,
    html_context: str = "", db_context: str = "") -> str:
        """
        修复测试代码

        Args:
        code: 原始测试代码
        error_info: 错误信息 {'error': '错误消息', 'traceback': '堆栈信息'}
        requirements: 产品需求文档
        html_context: 页面HTML上下文(可选)
        db_context: 数据库状态上下文(可选)
        """
        for attempt in range(self.max_retries):
            try:
                prompt = self._generate_fix_prompt(
                code, error_info, requirements, html_context, db_context
                )
                response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.1,
                max_tokens=4000
                )
                content = response.choices[0].message.content
                fixed_code = self._extract_code_block(content)

                if not fixed_code:
                    logger.warning(f"尝试 {attempt + 1}: 无法提取代码块,使用原始内容")
                    fixed_code = content.strip()

                # 验证修复后的代码基本结构
                if self._validate_code(fixed_code):
                    logger.info(f"修复成功 (尝试 {attempt + 1}/{self.max_retries})")
                    logger.info(f"代码长度: {len(fixed_code)} 字符")
                    return fixed_code
                else:
                    logger.warning(f"尝试 {attempt + 1}: 代码验证失败,重试...")

            except Exception as e:
                logger.error(f"修复尝试 {attempt + 1} 失败: {e}")
                if attempt == self.max_retries - 1:
                    logger.error("达到最大重试次数,返回原始代码")
                    return code
        return code

    def _validate_code(self, code: str) -> bool:
        """验证修复后的代码基本结构"""
        required_elements = [
            "def test",
            "self.page",
            "expect("
        ]

        # 修复逻辑:只有在发现缺失元素时才返回 False,否则循环结束后返回 True
        for element in required_elements:
            if element not in code:
                logger.warning(f"代码缺少必要元素: {element}")
                return False # 只有在缺失时才返回 False
        return True # 所有元素都存在,返回 True

generator.py

产生测试程序 python

代码语言:javascript
复制

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试代码生成器 - 调用 LLM 从需求生成 Playwright + Pytest 代码
"""
import os
import sys
import json
import re
import logging
from typing import List, Dict
from openai import OpenAI

logger = logging.getLogger(__name__)

class TestGenerator:
    def __init__(self):
        self.client = OpenAI(
            api_key=os.getenv("DASHSCOPE_API_KEY"),
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
        )
        self.model = os.getenv("QWEN_MODEL", "qwen-plus")
    
    def _sanitize_text(self, text: str) -> str:
        """清理文本中的特殊字符,但保留标点符号"""
        if not text:
            return text
        # 只移除表情符号和特殊 Unicode 字符,保留中文标点保留的标点:,。!?;:、""''()【】
        cleaned = re.sub(r'[^\u4e00-\u9fff\u3400-\u4dbf\u0000-\u007f\u0080-\u00ff\u3000-\u303f\uff00-\uffef]', '', text)
        return cleaned
    
    def _extract_code(self, content: str) -> str:
        """从 LLM 返回内容中提取代码"""
        content = content.strip()
    
        # 查找代码块
        if "```python" in content:
            start = content.find("```python") + 9
            end = content.find("```", start)
            if end != -1:
                return content[start:end].strip()
        elif "```" in content:
            parts = content.split("```")
            if len(parts) >= 2:
                code = parts[1].strip()
                # 移除语言标识
                lines = code.split("\n")
                if lines and lines[0].lower() in ["python", "py"]:
                    code = "\n".join(lines[1:]).lstrip()
                return code
        return content

    def generate(self, requirements: str) -> str:
        """让 LLM 直接生成 Playwright 测试代码 - 使用参数化形式"""
        
        # 清理输入
        requirements = self._sanitize_text(requirements)
        
        # 强调使用参数化的 Prompt
        prompt = f"""
            你是一个 Playwright + Pytest 自动化测试专家。请根据以下产品需求,直接生成可执行的端到端测试代码。

            ## 产品需求
            {requirements}

            ## 核心要求

            ## 标点符号保护规则(最高优先级 - 违反将导致测试失败)

            【严重警告】你必须严格遵守以下规则,否则测试代码将无法通过:

            1. 绝对保留所有标点符号:逗号(,)、句号(。)、感叹号(!)、问号(?)、分号(;)、冒号(:)、顿号(、)
            2. 严禁删除、替换或省略任何标点符号
            3. 错误消息必须与需求文档中的文本完全一致,包括标点符号

            【正确示例 - 必须这样做】:
               ERROR_MESSAGES = {{
                   "CONTACT_NOT_FOUND": "您输入的手机号或Email不存在,请重新输入!",
                   "VERICODE_ERROR": "验证码错误,请重新输入!",
                   "PASSWORD_USED_BEFORE": "这个密码以前设置过,请用一个新密码!",
               }}

            【错误示例 - 绝对禁止这样做】:
               ERROR_MESSAGES = {{
                   "CONTACT_NOT_FOUND": "您输入的手机号或Email不存在请重新输入",  # 错误!缺少逗号和感叹号
                   "VERICODE_ERROR": "验证码错误请重新输入",  # 错误!缺少逗号和感叹号
                   "PASSWORD_USED_BEFORE": "这个密码以前设置过请用一个新密码",  # 错误!缺少逗号和感叹号
               }}

            4. 所有断言中使用的错误消息字符串必须包含完整标点符号
            5. 生成 ERROR_MESSAGES 字典时,必须逐字复制需求文档中的错误消息,包括标点

            - 分析需求:理解需求中的功能点、业务流程、验证规则、边界条件
            - 设计测试用例:覆盖正常流程、异常流程、边界条件
            - 使用参数化:必须使用 `from parameterized import parameterized` 和 `@parameterized.expand`
            - 生成代码:为相似场景使用参数化,不同场景创建独立测试函数
            - 所有测试方法必须放在测试类中
            -  不测试字段为空的情形

            ## 技术规范

            - 使用 Playwright 同步 API:`from playwright.sync_api import Page, expect`
            - 使用 pytest 框架
            - 使用 `@pytest.fixture(autouse=True)` 在每个测试前后执行数据库清理
            - 使用 `@pytest.mark.usefixtures("db_cleanup")` 应用到测试类
            - 选择器优先级:
              * `page.get_by_role("button", name="按钮文字")` - 按角色定位
              * `page.get_by_label("字段标签")` - 按标签定位
              * `page.get_by_placeholder("提示文字")` - 按占位符定位
              * `page.locator("#id")` - 按 ID 定位
              * `page.locator(".class")` - 按class定位
            - URL 地址:根据需求中的描述推断,默认使用 `http://localhost:3000`
            - 等待策略:优先使用 `expect(...).to_be_visible()` 而非固定延时
            - 对于反向测试用例,一次经允许出现一处错误的数据
            - 判断URL请使用语句:assert self.page.url.startswith(REGISTER_URL)

            ## 代码模板

            ```
            import pytest
            from playwright.sync_api import Page, expect
            import pymysql

            DB_CONFIG = {{
                'host': 'localhost',
                'user': 'root',
                'password': '123456',
                'database': 'chatgptebusiness'
            }}

            REGISTER_URL = "http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RegisterPage.jsp"

            MESSAGES = {{
                "LOGIN_PAGE": "登录页面",
                "INVALID_USERNAME": "账号必须是5-20位的字母或数字",
                "INVALID_PASSWORD": "密码必须包含大小写字母数字和特殊字符,长度在5-30之间",            
                "INVALID_REPASSWORD": "密码确认不一致",
                "INVALID_PHONE": "请输入有效的中国手机号",
                "INVALID_EMAIL": "请输入有效的Email地址",
                "USERNAME_CONFLICT": "注册用户的用户名必须唯一",
                "PHONE_CONFLICT": "注册用户的手机必须唯一",
                "EMAIL_CONFLICT": "注册用户的邮箱必须唯一",
            }}

            class TestUserRegistration:
    
            @pytest.fixture(autouse=True)
            def setup_and_teardown(self, page: Page):
                \"\"\"每个测试前后执行数据库清理,并注入page\"\"\"
                self.page = page
                self.db = pymysql.connect(**DB_CONFIG)
                self._clear_tables()
                yield
                self.db.close()
            
            def _clear_tables(self):
                \"\"\"清空数据库表确保测试环境干净\"\"\"
                try:
                    with self.db.cursor() as cursor:
                        cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
                        cursor.execute("DELETE FROM user")
                        cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
                        self.db.commit()
                except Exception as e:
                    pytest.fail(f"数据库表清除失败: {{str(e)}}")
            
            def _fill_registration_form(self, username: str, password: str, 
                                       repassword: str, phone: str, email: str):
                \"\"\"填写注册表单并点击注册按钮 - 使用正确的定位器\"\"\"
                
                # 等待页面完全加载
                self.page.wait_for_load_state("networkidle")
                
                # 使用 ID 定位器
                self.page.locator("#username").fill(username)
                self.page.locator("#password").fill(password)
                self.page.locator("#confirmPassword").fill(repassword)
                self.page.locator("#phone").fill(phone)
                self.page.locator("#email").fill(email)
                
                # 点击注册按钮
                self.page.locator("button[type='submit']").click()
            
            @pytest.mark.parametrize("username,password,repassword,phone,email,expected_message", [
                # GTC-001: Valid registration
                ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", MESSAGES["LOGIN_PAGE"]),
                # GTC-002: Username too short
                ("abc", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", MESSAGES["INVALID_USERNAME"]),
                ...
                # GTC-007: Password too long
                ("jerrygu", "A" * 31 + "Zxcv123@", "A" * 31 + "Zxcv123@", "13687654321", "a@126.com", MESSAGES["INVALID_PASSWORD"]),
                ...
                ("jerrygu", "Zxcv123@", "Zxcv123@", "1368765432", "a@126.com", MESSAGES["INVALID_PHONE"]),
                ...
                ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a126@com", MESSAGES["INVALID_EMAIL"]),
                ...
            ])
            def test_registration_validation(self, username, password, repassword, phone, email, expected_message):
                \"\"\"测试注册表单各字段验证规则参数化\"\"\"
                self.page.goto(REGISTER_URL)
                
                # 等待页面加载
                self.page.wait_for_load_state("networkidle")
                
                # 填写表单
                self._fill_registration_form(username, password, repassword, phone, email)
                
                # 根据预期结果进行断言
                if expected_message == MESSAGES["LOGIN_PAGE"]:
                    # 成功注册,验证跳转到登录页
                    expect(self.page).to_have_title(MESSAGES["LOGIN_PAGE"], timeout=10000)
                    
                    # 验证用户已写入数据库
                    with self.db.cursor() as cursor:
                        cursor.execute("SELECT COUNT(*) FROM user WHERE username = %s", (username,))
                        count = cursor.fetchone()[0]
                        assert count == 1, f"Expected 1 user with username '{{username}}' but found {{count}}"
                else:
                    # 等待错误信息出现
                    
                    # 验证错误提示
                    if "账号必须是" in expected_message:
                        error_locator = self.page.locator("#usernameError")
                    elif "密码必须包含" in expected_message:
                        error_locator = self.page.locator("#passwordError")
                    elif "密码确认不一致" in expected_message:
                        error_locator = self.page.locator("#confirmPasswordError")
                    elif "请输入有效的中国手机号" in expected_message:
                        error_locator = self.page.locator("#phoneError")
                    elif "请输入有效的Email地址" in expected_message:
                        error_locator = page.locator("#emailError")
                    else:
                        error_locator = self.page.locator("#registerError")
                    
                    expect(error_locator).to_contain_text(expected_message, timeout=5000)
                    # 验证停留在注册页面
                    assert self.page.url.startswith(REGISTER_URL)

            @pytest.mark.parametrize("username,password,repassword,phone,email,dup_username,dup_phone,dup_email,expected_message", [
                # GTC-021: Duplicate username
                ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", 
                 "jerrygu", "18767896543", "b@126.com", MESSAGES["USERNAME_CONFLICT"]),
                # GTC-022: Duplicate phone
                ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", 
                 "peter", "13687654321", "c@126.com", MESSAGES["PHONE_CONFLICT"]),
                # GTC-023: Duplicate email
                ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", 
                 "peter", "18767896543", "a@126.com", MESSAGES["EMAIL_CONFLICT"]),
            ])
            def test_duplicate_constraint(self, username, password, repassword, phone, email,
                                          dup_username, dup_phone, dup_email, expected_message):
                \"\"\"测试唯一性约束:用户名、手机号、邮箱重复注册\"\"\"
                # 第一次注册 - 应该成功
                self.page.goto(REGISTER_URL)
                self.page.wait_for_load_state("networkidle")
                self._fill_registration_form(username, password, repassword, phone, email)
                expect(self.page).to_have_title(MESSAGES["LOGIN_PAGE"], timeout=10000)
                
                # 第二次注册 - 应该失败并显示冲突错误
                self.page.goto(REGISTER_URL)
                self.page.wait_for_load_state("networkidle")
                self._fill_registration_form(dup_username, password, repassword, dup_phone, dup_email)
                
                # 检查注册错误信息
                expect(self.page.locator("#registerError")).to_contain_text(expected_message, timeout=5000)
                assert self.page.url.startswith(REGISTER_URL)
                
                # 验证没有创建重复用户
                with self.db.cursor() as cursor:
                    cursor.execute("SELECT COUNT(*) FROM user WHERE username = %s OR phone = %s OR email = %s", 
                                  (dup_username, dup_phone, dup_email))
                    count = cursor.fetchone()[0]
                    assert count == 1, f"重复注册意外插入了用户,找到 {{count}} 条记录"

            def test_password_confirmation_mismatch_after_typing(self):
                \"\"\"测试密码确认框实时校验输入不一致时显示错误\"\"\"
                ...
                ```

                **重要提醒**

                - 测试类必须使用 @pytest.mark.usefixtures("db_cleanup") 装饰器
                - 测试类名使用 PascalCase,如 TestUserLogin、TestRegistrationPage
                - 测试方法名使用 snake_case,如 test_valid_login、test_password_validation
                - 如果测试方法不在类中使用参数化,则 page: Page 是唯一参数

                **输出要求**
                                
                - 只返回 Python 代码,不要任何解释
                - 代码必须可以直接执行
                - 测试函数命名使用英文,清晰描述测试场景
                - 添加必要的注释说明关键步骤
                - 不要使用任何表情符号或特殊 Unicode 字符
                - 优先使用 @pytest.mark.parametrize 处理多组测试数据
                - 请生成测试代码
        """
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.0,
            max_tokens=8000
        )
        content = response.choices[0].message.content
        content = self._sanitize_text(content)

        #提取代码
        code = self._extract_code(content)

        #确保有必要的导入
        if "from parameterized import parameterized" not in code:
            #在导入部分添加 parameterized
            if "from playwright.sync_api" in code:
                code = code.replace(
                    "from playwright.sync_api import Page, expect",
                    "from playwright.sync_api import Page, expect\nfrom parameterized import parameterized"
                )
            else:
                code = "import re\nimport pytest\nfrom playwright.sync_api import Page, expect\nfrom parameterized import parameterized\n\n" + code
        return code

    def fix(self, code: str, error_info: Dict, requirements: str) -> str:
        """修复错误的测试代码 - 保持参数化形式"""

        #清理输入
        requirements = self._sanitize_text(requirements)
        code = self._sanitize_text(code)
        error_msg = error_info.get('error', '无详细信息')

        prompt = f"""
            你是一个 Playwright + Pytest 自动化测试专家。下面的测试代码运行失败了,请分析错误并修复。

            **原始产品需求**

            {requirements}
            
   **当前测试代码(有错误)**
            
            ```
            {code}
            ```
            
   **实际运行错误**
            
            ```
            {error_msg}
            ```

            请根据错误信息输出修复后的完整 Python 代码,只返回代码。
            """
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.1,
                max_tokens=4000
            )
            content = response.choices[0].message.content
            content = self._sanitize_text(content)
            fixed_code = self._extract_code(content)

            #确保修复后的代码也包含 parameterized 导入
            if "from parameterized import parameterized" not in fixed_code:
                if "from playwright.sync_api" in fixed_code:
                    fixed_code = fixed_code.replace(
                        "from playwright.sync_api import Page, expect",
                        "from playwright.sync_api import Page, expect\nfrom parameterized import parameterized"
                    )
                else:
                    fixed_code = "from parameterized import parameterized\n" + fixed_code
            return fixed_code
        except Exception as e:
            logger.error(f"修复代码时出错: {e}")
            return self._generate_fallback(requirements)

    def _generate_fallback(self, requirements: str) -> str:
        """生成降级模板 - 使用参数化形式"""
        requirements = self._sanitize_text(requirements)
        return f'''
            """
            自动生成测试模板 - 请根据以下需求手动完善
            需求摘要:{requirements}
            **使用说明: **              
            使用 @pytest.mark.parametrize 装饰器组织测试数据
            根据需求中的功能点设计测试用例
            使用 Playwright 定位器选择页面元素
            添加适当的断言验证
            """
            import re
            import pytest
            from playwright.sync_api import Page, expect
            from parameterized import parameterized

            定义测试数据
            TEST_DATA = [
                ("正常流程", "valid_input", "expected_success"),
                ("异常流程", "invalid_input", "expected_error"),
            ]

            @pytest.mark.parametrize(TEST_DATA)
            def test_feature_scenarios(page: Page, scenario_name, test_input, expected_output):
                """参数化测试 - {scenario_name}"""
                导航到页面
                page.goto("http://localhost:3000/")
                TODO: 根据需求添加测试步骤
                page.get_by_label("字段名").fill(test_input)
                page.get_by_role("button", name="按钮文字").click()
                断言验证
                expect(page.get_by_text(expected_output)).to_be_visible()
                pass
            '''

orchestrator.py

总调度 python

代码语言:javascript
复制

#核心编排器
#!/usr/bin/env python
"""
测试编排器 - 实现"生成→运行→修复"闭环
"""
import os
import sys
import json
import time
import shutil
import logging
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, List, Tuple
import re

# 添加项目根目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))

from generator import TestGenerator
from executor import TestExecutor
from fixer import TestFixer

# ========== 日志配置 ==========
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(levelname)-8s | %(message)s',
    handlers=[
        logging.FileHandler('logs/test_run.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class TestOrchestrator:
    """测试编排器 - 协调生成、执行、修复的完整流程"""
    
    def __init__(
        self,
        req_file: str = "req.txt",
        max_retry: int = 5,
        headless: bool = True,
        output_dir: str = "outputs",
        llm_config: Optional[Dict] = None,
        custom_rules: Optional[str] = None
    ):
        self.req_file = Path(req_file)
        self.max_retry = max_retry
        self.headless = headless
        self.output_dir = Path(output_dir)
        self.history_dir = self.output_dir / "test_history"
        
        # 保存新增的参数
        self.llm_config = llm_config or {}
        self.custom_rules = custom_rules or ""
        
        # 确保目录存在
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.history_dir.mkdir(parents=True, exist_ok=True)
        Path("logs").mkdir(exist_ok=True)
        
        # 初始化组件 - 传递配置
        self.generator = TestGenerator()
        self.executor = TestExecutor(headless=headless)
        self.fixer = TestFixer()
        
        # 状态记录
        self.current_test_file = self.output_dir / "test_current.py"
        self.fix_report_file = self.output_dir / "fix_report.json"
        self.fix_history: List[Dict] = []
        self.iteration = 0
        self.preserve_punctuation = True
        
    def read_requirements(self) -> str:
        """读取需求文件"""
        if not self.req_file.exists():
            raise FileNotFoundError(f"需求文件不存在: {self.req_file}")
        content = self.req_file.read_text(encoding="utf-8")
        logger.info(f"已读取需求文件 ({len(content)} 字符)")
        return content
    
    def save_version(self, code: str, iteration: int, status: str):
        """保存代码版本到历史目录"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        version_file = self.history_dir / f"test_v{iteration:02d}_{status}_{timestamp}.py"
        version_file.write_text(code, encoding="utf-8")
        logger.debug(f"已保存版本: {version_file.name}")
    
    def run_single_iteration(self, requirements: str, previous_code: Optional[str] = None) -> Tuple[bool, str, Dict]:
        """
        执行单次迭代:生成/修复 → 运行 → 返回结果
        
        Returns:
            (success, code, execution_result)
        """
        self.iteration += 1
        logger.info(f"\n{'='*50}")
        logger.info(f"第 {self.iteration} 轮迭代开始")
        logger.info(f"{'='*50}")
        
        # Step 1: 生成或获取代码
        if previous_code is None:
            logger.info("首次生成测试代码...")
            code = self.generator.generate(requirements)
            action = "generate"
        else:
            logger.info("尝试修复代码...")
            # 需要上次执行的错误信息
            last_result = self.fix_history[-1] if self.fix_history else {}
            code = self.fixer.fix(previous_code, last_result, requirements)
            action = "fix"
        
        # 保存版本
        self.save_version(code, self.iteration, action)
        
        # 写入当前测试文件
        self.current_test_file.write_text(code, encoding="utf-8")
        logger.info(f"测试代码已写入: {self.current_test_file}")
        
        # Step 2: 执行测试
        logger.info("执行测试...")
        exec_result = self.executor.run(str(self.current_test_file))
        
        # Step 3: 记录结果
        record = {
            "iteration": self.iteration,
            "action": action,
            "timestamp": datetime.now().isoformat(),
            "success": exec_result["success"],
            "passed_tests": exec_result.get("passed", []),
            "failed_tests": exec_result.get("failed", []),
            "error": exec_result.get("error"),
            "exit_code": exec_result.get("exit_code")
        }
        self.fix_history.append(record)
        
        # 更新修复报告
        self._save_fix_report()
        
        # 打印结果
        if exec_result["success"]:
            logger.info(f"第 {self.iteration} 轮测试全部通过!")
            logger.info(f"通过用例: {exec_result.get('passed', [])}")
        else:
            logger.warning(f"第 {self.iteration} 轮测试失败")
            logger.warning(f"失败用例: {exec_result.get('failed', [])}")
            if exec_result.get("error"):
                logger.warning(f" 错误摘要: {exec_result['error'][:200]}...")
        
        return exec_result["success"], code, exec_result
    
    def _save_fix_report(self):
        """保存修复过程报告"""
        report = {
            "req_file": str(self.req_file),
            "max_retry": self.max_retry,
            "final_success": self.fix_history[-1]["success"] if self.fix_history else False,
            "total_iterations": self.iteration,
            "history": self.fix_history
        }
        self.fix_report_file.write_text(
            json.dumps(report, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def run(self) -> bool:
        """
        主流程:持续迭代直到全部通过或达到最大重试
        
        Returns:
            True 如果最终全部通过,False 如果达到上限仍有失败
        """
        logger.info("="*60)
        logger.info("启动测试编排器")
        logger.info(f"需求文件: {self.req_file}")
        logger.info(f"最大重试: {self.max_retry}")
        logger.info(f"无头模式: {self.headless}")
        logger.info("="*60)
        
        # 读取需求
        requirements = self.read_requirements()
        
        success = False
        current_code = None
        last_result = None
        
        # 迭代循环
        while self.iteration < self.max_retry:
            success, current_code, last_result = self.run_single_iteration(
                requirements, current_code
            )
            
            if success:
                break
        
        # 最终结果处理
        logger.info("\n" + "="*60)
        if success:
            logger.info(" 所有测试通过!")
            logger.info(f"最终代码: {self.current_test_file}")
            logger.info(f"总迭代次数: {self.iteration}")
            logger.info(f"修复报告: {self.fix_report_file}")
            return True
        else:
            logger.error(f"达到最大重试次数 ({self.max_retry}),仍有测试失败")
            logger.error(f"失败的用例: {last_result.get('failed', []) if last_result else '未知'}")
            logger.error(f"请查看修复报告: {self.fix_report_file}")
            return False


def main():
    import argparse
    parser = argparse.ArgumentParser(description="测试编排器 - 自动生成、执行、修复测试")
    parser.add_argument("--req_file", type=str, default="req.txt")
    parser.add_argument("--max_retry", type=int, default=5)
    parser.add_argument("--headless", action="store_true", default=True)
    parser.add_argument("--no-headless", action="store_false", dest="headless")
    parser.add_argument("--verbose", action="store_true")
    args = parser.parse_args()
    
    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)
    
    orchestrator = TestOrchestrator(
        req_file=args.req_file,
        max_retry=args.max_retry,
        headless=args.headless
    )
    
    success = orchestrator.run()
    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

SKILL.md

markdown

代码语言:javascript
复制

---
name: test-orchestrator
description: |
  端到端测试自动化编排器。
  读取需求 → 生成测试 → 执行 → 失败自动修复 → 重试,直至全部通过。
version: 1.0.0
author: AI Agent
---

# 测试编排技能

## 能力概述
本技能实现了一个闭环的测试自动化流程:
1. 从 `req.txt` 解析产品需求
2. 调用 LLM 设计测试用例并生成 Playwright + Pytest 代码
3. 自动执行生成的测试
4. 如果失败,分析错误并调用 LLM 修复代码
5. 重复步骤 3-4 直到全部通过或达到最大重试次数(默认 5 次)

## 输入参数
- `--req_file`: 需求文件路径,默认 `req.txt`
- `--max_retry`: 最大修复重试次数,默认 `5`
- `--headless`: 是否无头模式运行,默认 `true`
- `--verbose`: 详细日志输出

## 输出产物
- `outputs/test_current.py`: 最终通过的测试代码
- `outputs/fix_report.json`: 修复过程记录
- `logs/test_run.log`: 完整运行日志

## 编排流程
┌─────────────┐
│ 读取 req.txt              │
└──────┬──────┘
▼
┌─────────────┐
│ LLM 生成代码         │◄─────────────────┐
└──────┬──────┘ │
▼ │
┌─────────────┐ │
│ 执行测试 │               │
└──────┬──────┘ │
▼ │
┌───────┐ 否 │
│ 通过?       ├────────┐ │
└───┬───┘ ▼ │
│是 ┌─────────────┐ │
▼ │ 分析错误 │ │
┌────────┐ │ LLM 修复代码│────┘
│ 完成! │ └─────────────┘
└────────┘ (最多 5 次)
## 退出码
- `0`: 所有测试通过
- `1`: 达到最大重试次数仍有失败
- `2`: 需求文件不存在或格式错误

需求文档

注册需求

代码语言:javascript
复制

****基本需求****

输入username(#username)、password(#password)、confirmPassword(#confirmPassword)、phone(#phone)、email(#email)
单击【注册】按键
注册成功,进入登录页面,标题为“登录页面”,URL为:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/LoginPage.jsp

****格式要求****

- 账号(必填):文本框,长度为5-20位,可以包含大小写英文字符(必填)或数字(选填)。
<label for="username">账号 (5-20位字母或数字):</label>
<input type="text" id="username" name="username" placeholder="输入账号" autocomplete="username" required>
- 密码(必填):密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符,密码通过SHA256散列进行传输和存储。
<label for="password">密码 (5-30位,包含大小写字母、数字和特殊字符):</label>
<input type="password" id="password" name="password" placeholder="输入密码" autocomplete="current-password" required>
- 确认密码(必填):密码框,确认密码的值必须与密码的值一致。
<label for="confirmPassword">密码确认:</label>
<input type="password" id="confirmPassword" name="confirmPassword"  placeholder="输入确认密码" autocomplete="current-password" required>
- 手机号(必填):手机框,需符合中国手机号码格式。
<label for="phone">手机号 (中国):</label>
<input type="tel" id="phone" name="phone"  placeholder="输入手机号" autocomplete="tel" required>
- Email(必填):Email框,需符合国际标准Email格式
<label for="email">邮箱:</label>
<input type="email" id="email" name="email"  placeholder="输入邮箱" autocomplete="email" required>
<label for="email">邮箱:</label>

****错误提示****

-  输入非法格式的账号,比如长度<5位、长度<20位 、不包含大小写英文字符,提交后报'账号必须是5-20位的字母或数字'错误信息,停留在注册页面
<div id="usernameError" class="error"></div>
-  输入非法格式的密码,比如长度<5位、长度<30位 、不包含大小写英文字符、数字、特殊字符,提交后报'密码必须包含大小写字母数字和特殊字符,长度在5-30之间'错误信息,停留在注册页面
<div id="passwordError" class="error"></div>
-  输入合法的密码,但是确认密码与密码不一致,提交后报'密码确认不一致'错误信息,停留在注册页面
<div id="confirmPasswordError" class="error"></div>
-  输入非法格式的手机号,比如长度不为11的数字、非1开始的数值,电话号中含有大小写英文字符,提交后报'请输入有效的中国手机号'错误信息,停留在注册页面
<div id="phoneError" class="error"></div>
-  输入非法格式的email,比如a@com邮件(其他错误格式HTML5会自动校验)、提交后报'请输入有效的Email地址'错误信息,停留在注册页面
<div id="emailError" class="error"></div>
-  注册的用户账号已经存在,报'注册用户的用户名必须唯一'错误信息,停留在注册页面
-  注册的手机已经存在,报'注册用户的手机必须唯一'错误信息,停留在注册页面
-  注册的Email已经存在,报'注册用户的邮箱必须唯一'错误信息,停留在注册页面
<div id="registerError" class="error"></div>
- 测试注册按钮在必填字段为空时不被禁用

****数据库信息****

# 测试环境配置
SQL  
```
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': '123456',
    'database': 'chatgptebusiness'
}
- user表格式
   id int 自增1
   username varchar(50)
   password varchar(100)
   phone         varchar(50)
   email         varchar(50) 
```
- 请执行每个测试用例前,建立数据库连接,清除user表;执行每个测试用例后,请断开数据库连接
- 注册用户成功,请进入数据库中进行检查,注册的数据是否正确存在数据库中
****URL****
- url=http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RegisterPage.jsp

需求包括:

  • 基本需求:描述 正常情况
  • 格式要求:包括每个字段的格式以及在HTML中的位置
  • 错误提示:什么情况下报什么错误,错误出现在什么位置
  • 数据库信息:由于在测试中需要操作数据库,所有必须把数据库信息告诉智能体
  • URL:产品信息关系到的URL

登录需求

代码语言:javascript
复制

****基本需求****

- 输入username(#username)、password(#password)单击【登录】按键
- 登录成功,进入欢迎页面,标题为“系统欢迎您”,URL为:127.0.0.1:8080/ChatGPTEbusiness/jsp/WelcomePage.jsp

****格式要求****

- 用户名(必填):文本框,长度为5-20位,必须包含大写或小写英文字符,也可以包含数字,不允许包含除大小写英文字符、数字其他外的字符(/^[a-zA-Z0-9]{5,20}$/)。
<label for="username">用户名:</label>
<input type="text" id="username" name="username" placeholder="输入账号" autocomplete="username" required>
- 密码(必填):密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/)。密码通过SHA256散列进行传输和存储。
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="输入密码" autocomplete="current-password" required>

****错误提示****

-  输入非法格式的账号,比如长度<5位、长度<20位 、不包含大小写英文字符,报'账号必须是5-20位的字母或数字'错误信息,停留在注册页面
<div id="usernameError" class="error"></div>
-  输入非法格式的密码,比如长度<5位、长度<30位 、不包含大小写英文字符、数字、特殊字符,报'密码必须包含大小写字母数字和特殊字符,长度在5-30之间'错误信息,停留在注
册页面
- 用户名或密码错误
<div id="loginError" class="error"></div>
- 登录失败

****数据库信息****

SQL  
```
# 测试环境配置
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': '123456',
    'database': 'chatgptebusiness'
}
- user表格式
   id int 自增1
   username varchar(50)
   password varchar(100)
   phone         varchar(50)
   email         varchar(50) 
```
- 请执行每个测试用例前,建立数据库连接,在user表中插入一条user表信息(password通过SHA256散列);执行每个测试用例后,请断开数据库连接

****URL****

- url=http://127.0.0.1:8080/ChatGPTEbusiness/jsp/LoginPage.jsp

注意

  • 登录需求与注册需求相当,变动对应部分即可

找回密码需求

代码语言:javascript
复制

***找回密码产品需求***

- 1.进入输入手机号或Email页面
- 2.如果手机号或Email格式不正确,前端返回:"请输入有效的中国手机号或Email"
- 3.将手机号或Email传入后端,后端验证是否存在这个手机号或Email
- 4.如果不存在后端返回:"您输入的手机号或Email不存在,请重新输入!" **注意**不是“您输入的手机号或Email不存在请重新输入”
- 5.如果手机号存在,记录这个手机号所属于的用户ID号(uid);如果Email存在,记录这个Email所属于的用户ID号(uid)
- 6.生成验证码(6位数字),存储在表code中,值为(id,uid,code),其中id自增一、uid用户外键、code验证码
- 7.如果是手机号向用户手机发送验证码;如果是Email向Email发送验证码。
- 8.前端进入重置密码页面
- 9.用户输入验证码(6位数字)、新密码(长度为5-30位,必须包含大小写英文字符、数字和特殊字符,密码通过SHA256散列进行传输和存储)和新密码确认码。
- 10.验证码不符合格式,前端返回:"验证码必须是6位数字"
- 11. 新密码不符合格式,前端返回:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"
- 12.新密码与新密码确认码,前端返回:"密码确认不一致!"
- 13.将验证码、新密码与新密码确认码传入后端
- 14. 如果验证码不正确,后端返回提示信息:"验证码错误"。
- 15.与password比较,如果新密码以前使用过,后端返回提示信息:"新密码以前使用过,请设置其他密码"。
- 16.否则,将旧密码添加至密码历史password表中(id,uid,password)。其中id自增一、uid用户外键、password旧密码。
- 17.将新密码更新至当前用户user表中。
- 18.删除验证码code表中的临时验证码记录。
- 19.设置成功后,重定向至登录页面。

***各字段格式要求***

****VeriCodePage.jsp****

- 手机号码或电子邮箱,格式:手机号码或电子邮箱
 <label for="contact">手机号码或电子邮箱:</label>
 <input type="text" id="contact" name="contact" placeholder="输入手机号码或邮箱" required>
- 提交按钮:<button type="submit" id="sendCode">发送验证码</button>
- 网页标题:输入手机号码或电子邮箱 验证请用:assert CONTRACT_URL in self.page.url

****RecoverPage.jsp****

- 验证码:6位数字:/^\d{6}$/
 <label for="identifyingCode">验证码:</label>
 <input type="text" id="identifyingCode" name="identifyingCode" placeholder="输入验证码" maxlength="6" required>
- 新密码,密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/)
 <label for="newPassword">新密码:</label>
  <input type="password" id="newPassword" name="newPassword" placeholder="输入新密码" required>
- 新密码确认码,密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/)
 <label for="confirmPassword">确认新密码:</label>
 <input type="password" id="confirmPassword" name="confirmPassword" placeholder="确认新密码" required> 
- 提交按钮
 <button type="submit">确定</button>
- 建议:定位"输入新密码"与"确认新密码"使用
 self.page.locator("#newPassword").fill(password)
 self.page.locator("#confirmPassword").fill(confirm_password)
- 网页标题:找回密码 验证请用:assert VERICODE_URL in self.page.url

***断言信息***

****VeriCodePage.jsp****

- 提交成功,进入重置密码页面,页面"找回密码"
- 您输入的手机号或Email不存在,请重新输入!**注意**不是“您输入的手机号或Email不存在请重新输入”
 <div id="VeriCodeError" class="error"></div>
- 不考虑字段为空的情形

****RecoverPage.jsp****

- 提交成功,进入登录页面,页面"登录页面"
- 验证码必须是6位数字
 <div id="identifyingCodeError" class="error"></div>
- 密码必须包含大小写字母、数字和特殊字符,长度在5-30之间
 <div id="newPasswordError" class="error"></div>
- 密码确认不一致!
 <div id="confirmPasswordError" class="error"></div>
- 验证码错误,请重新输入!
- 这个密码以前设置过,请用一个新密码!
 <div id="recoverError" class="error"></div>
- 不考虑字段为空的情形

***数据库信息***

```
    DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': '123456',
    'database': 'chatgptebusiness'
}
```

****user表格式****

SQL  
```
 CREATE TABLE IF NOT EXISTS user(
 id INT AUTO_INCREMENT PRIMARY KEY,
 username VARCHAR(50) NOT NULL,
 password VARCHAR(100) NOT NULL,
 phone VARCHAR(50) NOT NULL,
 email VARCHAR(50) NOT NULL
);

 ``` 

 ****code表格式****

SQL  
 ```
 CREATE TABLE code(
 id INT AUTO_INCREMENT PRIMARY KEY,
 uid INT NOT NULL,
 code CHAR(6) NOT NULL,
 FOREIGN KEY(uid) REFERENCES user(id)
);
```

****password表格式****

SQL  
```
CREATE TABLE password(
 id INT AUTO_INCREMENT PRIMARY KEY,
 uid INT NOT NULL,
 password VARCHAR(100) NOT NULL,
 FOREIGN KEY(uid) REFERENCES user(id)
);
```
- 发送验证码存入code表
- 当输入的新密码验证正确,将旧密码存入password表,更新用户表中密码,删除code表中数据。

***URL***

- 输入phone或者Email:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/VeriCodePage.jsp
- 重置密码:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RecoverPage.jsp

***测试流程***

****正确流程1****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的手机信息。
- 3,点击【发送验证码】按键。
- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。
- 6,点击【确认】按键。
- 7,进入登录页面,页面标题为“登录页面”

****正确流程2****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的Email信息。
- 3,点击【发送验证码】按键。
- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。
- 6,点击【确认】按键。
- 7,进入登录页面,页面标题为“登录页面”

****错误流程1:非有效的手机号和Email字符****

- 1,在VeriCodePage.jsp中输入非手机非Email字符,比如jerrygu。
- 2,点击【发送验证码】按键。
- 3,在<div id="contactError" class="error"></div>显示"请输入有效的中国手机号或Email"

****错误流程2:手机号或Email不存在****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入非第1步用户信息的Email和Phone信息。
- 3,点击【发送验证码】按键。
- 4,在<div id="VeriCodeError" class="error"></div>显示"您输入的手机号或Email不存在,请重新输入!" **注意**不是“您输入的手机号或Email不存在请重新输入”

****错误流程3:验证码格式错误****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,输入验证码:A12345、新密码:Zxcv@123、确认新密码:Zxcv@123。
- 5,点击【确定】按钮
- 6,在<div id="identifyingCodeError" class="error"></div>显示:"验证码必须是6位数字"

****错误流程4:验证码不是所生成的****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,输入验证码:123456、新密码:Zxcv@123:Zxcv@123。
- 5,点击【确定】按钮
- 6,在<div id="recoverError" class="error"></div>显示:"验证码错误,请重新输入!"

****错误流程5:密码确认不一致!****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@124。
- 6,点击【确定】按钮
- 7,在<div id="confirmPasswordError" class="error"></div>显示:"密码确认不一致!"

****错误流程6:密码以前设置过****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。
- 6,在password表中插入(uid与SHA256后的字符Zxcv@123)。
- 7,点击【确定】按钮
- 8,在<div id="recoverError" class="error"></div>显示:"这个密码以前设置过,请用一个新密码!"

****错误流程7:新密码无特殊字符****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入验证码、新密码:Zxcv123、确认新密码:Zxcv123。
- 6,点击【确定】按钮
- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"

****错误流程8:新密码无大写字符****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入验证码、新密码:zxcv@123、确认新密码:zxcv@123。
- 6,点击【确定】按钮
- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"

****错误流程9:新密码无小写字符****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入验证码、新密码:ZXCV@123、确认新密码:ZXCV@123。
- 6,点击【确定】按钮
- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"

****错误流程10:新密码无数字字符****

- 1,向user表中注册表一条用户信息。
- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。
- 3,点击【发送验证码】按键。
- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码
- 5,在RecoverPage.jsp验证码中输入验证码、新密码:ZXCV@、确认新密码:ZXCV@。
- 6,点击【确定】按钮
- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"
  • 由于这个功能的流程比较复杂,需要给出各种情况,相当一use case

注意

  • 找回密码功能比较复杂,所以加上了“测试流程”一个环节。
  • 执行main.py的时候将注册需求、登录需求、找回密码需求,定义在名为req.txt文件中
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档