Aone.Net

学无止境
AI-移动端自动化框架构建 — 详细步骤与注意事项

 


📋 总体路线图

 
1Phase 1: 环境准备与技术选型(1天)
2    ↓
3Phase 2: 项目骨架搭建(0.5天)
4    ↓
5Phase 3: 核心框架开发(3天)
6    ├─ 3.1 驱动管理层
7    ├─ 3.2 Page Object基类
8    ├─ 3.3 工具层(日志/截图/ADB)
9    └─ 3.4 配置管理
10    ↓
11Phase 4: 用例开发实践(2天)
12    ↓
13Phase 5: CI/CD集成(1天)
14    ↓
15Phase 6: 优化与演进(持续)
16

🚀 Phase 1: 环境准备与技术选型

1.1 技术栈选型对比

维度方案A(推荐)方案B方案C
语言 Python 3.10+ Java Kotlin
核心框架 Appium 2.x + pytest Appium + TestNG Espresso + XCTest
页面模式 Page Object + Screenplay POM MVP
断言 pytest + Allure AssertJ + ExtentReport 原生断言
设备管理 ADB + uiautomator2 Selenium Grid Firebase Test Lab
适合场景 跨平台、团队协作 大型企业、Java团队 原生性能测试

💡 推荐方案A的理由:

  • Python上手快,测试人员友好
  • pytest生态强大(参数化、插件、并行)
  • Appium 2.x原生支持多设备
  • Allure报告美观,CI集成简单

1.2 环境安装(详细步骤)

bash
1# ============ 1. 系统依赖 ============
2# macOS
3brew install node appium python@3.10
4
5# Ubuntu/Debian
6sudo apt-get install -y openjdk-11-jdk python3.10 python3-pip nodejs npm
7sudo npm install -g appium@2
8
9# Windows
10# 下载并安装:Java JDK 11、Python 3.10、Node.js
11# 然后:npm install -g appium@2
12
13# ============ 2. Appium服务端 ============
14appium driver install uiautomator2  # Android
15appium driver install xcuitest      # iOS(仅macOS)
16
17# 验证安装
18appium --version  # 应输出 2.x.x
19appium driver list  # 查看已安装驱动
20
21# ============ 3. Android环境 ============
22# 安装Android SDK + Platform Tools
23sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0"
24
25# 验证ADB
26adb devices  # 应看到设备列表
27
28# ============ 4. iOS环境(仅macOS) ============
29# Xcode 15+(包含iOS模拟器)
30xcode-select --install
31
32# 验证
33xcrun simctl list devices  # 查看模拟器
34
35# ============ 5. Python依赖 ============
36pip install -U pip
37pip install appium==2.11.1 pytest==8.0.0 pytest-xdist==3.5.0
38pip install allure-pytest==2.13.3
39pip install pyyaml==6.0.1 python-dotenv==1.0.0
40pip install faker==24.2.0  # 测试数据生成
41pip install Pillow==10.2.0  # 图片处理
42
43# ============ 6. 验证环境 ============
44# 启动Appium服务
45appium --allow-insecure chromedriver_autodownload
46
47# 测试连接Android设备
48adb devices | grep -v "List" | wc -l  # 应 > 0
49
50# 测试连接iOS模拟器
51xcrun simctl list | grep Booted
52

⚠️ 注意事项:

问题原因解决方案
appium命令找不到 npm全局路径未加入PATH export PATH=$PATH:$(npm bin -g)
ADB连接失败 开发者模式未开/USB调试未开 手机设置→开发者选项→USB调试
iOS WDA启动失败 Xcode版本不匹配 更新Xcode或降级Appium
uiautomator2安装失败 网络问题 使用--registry=https://registry.npmmirror.com
Appium 2.x报错 旧版残留 appium driver uninstall uiautomator2重装

📁 Phase 2: 项目骨架搭建

2.1 目录结构设计

bash
1mobile-automation/
2├── 📄 README.md                    # 项目说明(必写)
3├── 📄 requirements.txt              # 依赖
4├── 📄 pyproject.toml               # 项目配置(替代setup.py)
5├── 📄 pytest.ini                   # pytest配置
6├── 📄 .env.example                 # 环境变量模板
7├── 📄 .gitignore                   # Git忽略
8├── 📁 config/                      # 配置文件
9│   ├── 📄 __init__.py
10│   ├── 📄 capabilities.yaml        # 设备能力配置
11│   ├── 📄 locators.yaml            # 元素定位
12│   └── 📄 test_data.yaml           # 测试数据
13├── 📁 framework/                   # 框架核心(不依赖业务)
14│   ├── 📄 __init__.py
15│   ├── 📄 base_page.py             # Page Object基类
16│   ├── 📄 driver_manager.py        # 驱动管理
17│   ├── 📄 config_loader.py        # 配置加载
18│   ├── 📄 logger.py                # 日志工具
19│   └── 📄 exceptions.py            # 自定义异常
20├── 📁 pages/                       # 页面对象(按业务模块)
21│   ├── 📄 __init__.py
22│   ├── 📄 base_page.py             # 页面基类(业务相关)
23│   ├── 📄 login_page.py
24│   ├── 📄 home_page.py
25│   └── 📄 mine_page.py
26├── 📁 tests/                       # 测试用例
27│   ├── 📄 __init__.py
28│   ├── 📄 conftest.py              # pytest fixtures
29│   ├── 📁 smoke/                   # 冒烟测试
30│   │   └── 📄 test_smoke.py
31│   ├── 📁 login/                   # 登录模块
32│   │   └── 📄 test_login.py
33│   └── 📁 order/                   # 下单模块
34│       └── 📄 test_order_flow.py
35├── 📁 utils/                       # 工具函数
36│   ├── 📄 __init__.py
37│   ├── 📄 adb_helper.py            # ADB封装
38│   ├── 📄 screenshot.py            # 截图/录屏
39│   ├── 📄 api_helper.py            # 接口请求
40│   └── 📄 data_factory.py          # 数据工厂
41├── 📁 reports/                     # 测试报告(.gitignore)
42│   ├── 📁 screenshots/
43│   ├── 📁 logs/
44│   └── 📁 allure-results/
45├── 📁 logs/                        # 运行日志(.gitignore)
46├── 📁 .github/                     # GitHub配置
47│   └── 📄 workflows/
48│       └── 📄 mobile_test.yml      # CI配置
49└── 📁 docs/                        # 文档
50    ├── 📄 framework_design.md
51    └── 📄 api.md
52

2.2 核心配置文件

📄 pyproject.toml

toml
1[tool.pytest.ini_options]
2testpaths = ["tests"]
3python_files = ["test_*.py"]
4python_classes = ["Test*"]
5python_functions = ["test_*"]
6addopts = """
7    -v
8    --strict-markers
9    --alluredir=reports/allure-results
10    --html=reports/report.html
11    --self-contained-html
12"""
13markers = [
14    "smoke: 冒烟测试用例",
15    "regression: 回归测试用例",
16    "flaky: 不稳定用例(需重试)",
17    "android: 仅Android",
18    "ios: 仅iOS",
19]
20
21[tool.coverage.run]
22source = ["framework", "pages"]
23omit = ["tests/*", "utils/adb_helper.py"]
24
25[tool.black]
26line-length = 100
27target-version = ["py310"]
28

📄 config/capabilities.yaml

yaml
1default:
2  platformName: Android
3  automationName: UiAutomator2
4  autoGrantPermissions: true
5  noReset: true  # 保留App数据(调试用)
6  fullReset: false
7
8android:
9  deviceName: emulator-5554
10  platformVersion: "13"
11  appPackage: com.example.app
12  appActivity: .MainActivity
13  udid: emulator-5554
14
15ios:
16  deviceName: iPhone 15
17  platformVersion: "17.2"
18  app: /path/to/app.app
19  udid: A1B2C3D4-E5F6-7890-ABCD-EF1234567890
20  wdaLocalPort: 8100
21
22# 多设备配置(并行执行用)
23devices:
24  - name: Pixel6_Android13
25    udid: emulator-5554
26    platform: Android
27    port: 4723
28  - name: Pixel8_Android14
29    udid: emulator-5556
30    platform: Android
31    port: 4725
32  - name: iPhone15_iOS17
33    udid: A1B2C3D4
34    platform: iOS
35    port: 8100
36

📄 config/locators.yaml(元素定位)

yaml
1# 登录页
2login:
3  phone_input:
4    android: id=com.example.app:id/et_phone
5    ios: predicate="type == 'XCUIElementTypeTextField' AND placeholderValue == '请输入手机号'"
6  send_code_btn:
7    android: id=com.example.app:id/btn_send
8    ios: accessibility_id=发送验证码
9  code_input:
10    android: id=com.example.app:id/et_verify_code
11    ios: predicate="type == 'XCUIElementTypeSecureTextField'"
12  login_btn:
13    android: text=登录
14    ios: label=登录
15
16# 首页
17home:
18  search_bar:
19    android: id=com.example.app:id/et_search
20    ios: predicate="type == 'XCUIElementTypeTextField'"
21  tab_mine:
22    android: id=com.example.app:id/tab_mine
23    ios: accessibility_id=我的
24
25# 通用定位策略(降级方案)
26fallback:
27  - by: id
28  - by: accessibility_id
29  - by: xpath
30  - by: class_name
31

⚠️ 注意事项:

配置项常见错误正确做法
noReset: true 缓存导致测试不干净 冒烟测试用true,回归用false
appPackage 包名写错 用adb shell pm list packages确认
wdaLocalPort iOS端口冲突 每个模拟器用不同端口(8100, 8101...)
locators.yaml 硬编码不利于维护 用YAML分层管理,支持环境覆盖
pytest.ini marker写错 marker名称统一小写,用前先定义

🔧 Phase 3: 核心框架开发

3.1 驱动管理层(最高优先级)

📄 framework/driver_manager.py

python
1"""
2Appium驱动管理器
3- 单例模式,全局唯一
4- 支持多设备并行
5- 自动生命周期管理(启动/退出)
6- 失败自动重连
7"""
8import time
9import logging
10from typing import Optional, Dict, Any
11from appium import webdriver
12from appium.options.android import UiAutomator2Options
13from appium.options.ios import XCUITestOptions
14from selenium.common.exceptions import WebDriverException
15
16logger = logging.getLogger(__name__)
17
18
19class DriverManager:
20    """Appium驱动单例管理器"""
21    
22    _instance: Optional['DriverManager'] = None
23    _drivers: Dict[str, webdriver.Remote] = {}  # udid -> driver
24    
25    def __new__(cls):
26        if cls._instance is None:
27            cls._instance = super().__new__(cls)
28        return cls._instance
29    
30    def __init__(self):
31        if not hasattr(self, '_initialized'):
32            self._initialized = True
33            self.config = self._load_config()
34    
35    def _load_config(self) -> dict:
36        """加载配置"""
37        import yaml
38        with open('config/capabilities.yaml', 'r', encoding='utf-8') as f:
39            return yaml.safe_load(f)
40    
41    def get_driver(self, device_name: str = "default") -> webdriver.Remote:
42        """
43        获取驱动(自动复用已有连接)
44        
45        Args:
46            device_name: 设备名称(对应config/capabilities.yaml中的key)
47        
48        Returns:
49            Appium WebDriver实例
50        
51        Raises:
52            ConnectionError: 无法连接设备
53        """
54        # 检查是否已有连接
55        if device_name in self._drivers:
56            try:
57                # 验证连接是否有效
58                self._drivers[device_name].current_context
59                logger.info(f"复用已有驱动: {device_name}")
60                return self._drivers[device_name]
61            except WebDriverException:
62                logger.warning(f"驱动失效,重新连接: {device_name}")
63                self.quit_driver(device_name)
64        
65        # 创建新连接
66        caps = self.config.get(device_name, self.config['default'])
67        driver = self._create_driver(caps)
68        self._drivers[device_name] = driver
69        
70        # 初始化设置
71        driver.implicitly_wait(5)
72        driver.set_page_load_timeout(30)
73        
74        logger.info(f"新建驱动: {device_name}, UDID={driver.capabilities.get('udid')}")
75        return driver
76    
77    def _create_driver(self, caps: dict)-> webdriver.Remote:78"""根据能力配置创建驱动"""79        platform = caps.get('platformName','Android')80        
81if platform =='iOS':82            options = XCUITestOptions()83else:84            options = UiAutomator2Options()85        
86# 动态设置能力87for key, value in caps.items():88try:89setattr(options, key.lower().replace('_',''), value)90except AttributeError:91                logger.warning(f"忽略未知能力: {key}")92        
93# 启动Appium服务94        appium_server ="http://localhost:4723"95        
96try:97            driver = webdriver.Remote(appium_server, options=options)98# 等待App启动完成99            time.sleep(3)100return driver
101except Exception as e:102            logger.error(f"驱动创建失败: {e}")103raise ConnectionError(f"无法连接设备: {caps.get('udid','unknown')}")104    
105defquit_driver(self, device_name:str="default"):106"""关闭驱动"""107if device_name in self._drivers:108try:109                self._drivers[device_name].quit()110                logger.info(f"已关闭驱动: {device_name}")111except Exception as e:112                logger.error(f"关闭驱动失败: {e}")113finally:114del self._drivers[device_name]115    
116defquit_all(self):117"""关闭所有驱动"""118for device_name inlist(self._drivers.keys()):119            self.quit_driver(device_name)120
121
122# 全局实例(方便导入使用)123driver_manager = DriverManager()124

📄 tests/conftest.py(pytest fixtures)

python
1"""
2pytest全局配置
3- 自动注入driver
4- 自动截图(失败时)
5- 测试数据清理
6"""
7import pytest
8import allure
9from framework.driver_manager import driver_manager
10from utils.screenshot import auto_screenshot
11from utils.logger import test_logger
12
13
14@pytest.fixture(scope="session")
15def app_driver(request):
16    """
17    Session级别的驱动
18    - 支持多设备并行:pytest-xdist会为每个worker创建独立session
19    """
20    device_name = request.config.getoption("--device", default="default")
21    driver = driver_manager.get_driver(device_name)
22    
23    yield driver
24    
25    # 清理:session结束时不自动quit(避免影响其他用例)
26    # 具体quit逻辑在用例级别控制
27
28
29@pytest.fixture(scope="function")
30def driver(app_driver):
31    """
32    Function级别的驱动
33    - 每个用例独立
34    - 自动重置App状态
35    """
36    # 重置App(清除缓存)
37    app_driver.reset_app()
38    time.sleep(2)
39    
40    yield app_driver
41    
42    # 用例结束后的清理
43    try:
44        # 关闭所有弹窗/键盘
45        app_driver.hide_keyboard()
46    except:
47        pass
48
49
50@pytest.fixture(autouse=True)
51def test_setup(driver, request):
52    """每个用例自动执行的前置操作"""
53    test_logger.info(f"开始执行: {request.node.nodeid}")
54    
55    # 初始化日志
56    test_logger.set_test_name(request.node.nodeid)
57    
58    yield
59    
60    # 后置操作
61    test_logger.info(f"结束执行: {request.node.nodeid}")
62
63
64@pytest.hookimpl(tryfirst=True, hookwrapper=True)
65def pytest_runtest_makereport(item, call):
66    """
67    pytest钩子:用例执行后自动截图
68    """
69    outcome = yield
70    report = outcome.get_result()
71    
72    if report.when == "call" and report.failed:
73        # 失败时自动截图
74        try:
75            driver = item.funcargs.get('driver')
76            if driver:
77                screenshot = auto_screenshot(driver, report.nodeid)
78                allure.attach.file(
79                    screenshot,
80                    name="失败截图",
81                    attachment_type=allure.attachment_type.PNG
82                )
83                # 附加错误日志
84                if report.longrepr:
85                    allure.attach(
86                        str(report.longrepr),
87                        name="错误详情",
88                        attachment_type=allure.attachment_type.TEXT
89                    )
90        except Exception as e:
91            test_logger.error(f"截图失败: {e}")
92
93
94# ========== 标记注册 ==========
95defpytest_load_initial_conftests(early_config):96    early_config.addinivalue_line(97"markers","smoke: 冒烟测试用例",98)99    early_config.addinivalue_line(100"markers","regression: 回归测试用例",101)102    early_config.addinivalue_line(103"markers","flaky: 不稳定用例(自动重试2次)",104)105
106
107@pytest.fixture108defdevice(request):109"""支持命令行指定设备: pytest --device=Pixel6_Android13"""110return request.config.getoption("--device", default="default")111

⚠️ 驱动管理注意事项:

问题原因解决方案
多设备执行时driver冲突 单例在多进程下不共享 用pytest-xdist + --dist loadgroup
驱动连接泄漏 用例失败未quit 用conftest.py的fixture自动清理
iOS WDA崩溃 内存不足/Xcode版本 定期重启模拟器:xcrun simctl shutdown
Android uiautomator2崩溃 页面复杂/动画多 降级到UiAutomator1或Espresso
端口占用 多个Appium实例 每个实例用不同端口:appium -p 4723

3.2 Page Object基类

📄 framework/base_page.py(框架层)

python
1"""
2Page Object基类(框架层)
3- 封装所有Appium通用操作
4- 不依赖具体业务
5- 所有页面类继承此类
6"""
7import time
8import logging
9from typing import Optional, Tuple, List
10from appium.webdriver.webdriver import WebDriver
11from appium.webdriver.common.appiumby import AppiumBy
12from selenium.webdriver.support.ui import WebDriverWait
13from selenium.webdriver.support import expected_conditions as EC
14from selenium.common.exceptions import (
15    TimeoutException, 
16    NoSuchElementException, 
17    StaleElementReferenceException
18)
19
20logger = logging.getLogger(__name__)
21
22
23class BasePage:
24    """页面基类 - 框架层"""
25    
26    # 默认等待时间(子类可覆盖)
27    DEFAULT_TIMEOUT = 10
28    
29    def __init__(self, driver: WebDriver):
30        self.driver = driver
31        self.wait = WebDriverWait(driver, self.DEFAULT_TIMEOUT)
32        self.logger = logging.getLogger(f"{self.__class__.__name__}")
33    
34    # ========== 元素定位 ==========
35    def find_element(self, locator: Tuple[str, str], timeout: Optional[int] = None) -> WebDriver:
36        """
37        显式等待元素出现
38        
39        Args:
40            locator: (by, value) 元组,如 (AppiumBy.ID, "com.example:id/btn")
41            timeout: 超时时间,默认使用类的DEFAULT_TIMEOUT
42        
43        Returns:
44            WebElement
45        
46        Raises:
47            TimeoutException: 超时未找到
48        """
49        timeout = timeout or self.DEFAULT_TIMEOUT
50        try:
51            by, value = locator
52            element = self.wait.until(EC.presence_of_element_located(locator))
53            self.logger.debug(f"找到元素: {locator}")
54            return element
55        except TimeoutException:
56            self.logger.error(f"元素定位超时: {locator}, timeout={timeout}s")
57            self._save_page_source(f"timeout_{locator[1]}")
58            raise
59    
60    def find_elements(self, locator: Tuple[str, str]) -> List[WebDriver]:
61        """查找多个元素"""
62        return self.driver.find_elements(*locator)
63    
64    # ========== 通用操作 ==========
65    def click(self, locator: Tuple[str, str], timeout: Optional[int] = None):
66        """点击元素(带等待)"""
67        element = self.find_element(locator, timeout)
68        # 等待元素可点击
69        self.wait.until(EC.element_to_be_clickable(locator))
70        time.sleep(0.3)  # 防止快速点击失效
71        element.click()
72        self.logger.info(f"点击元素: {locator}")
73    
74definput_text(self, locator: Tuple[str,str], text:str, clear:bool=True):75"""输入文本"""76        element = self.find_element(locator)77if clear:78            element.clear()79            time.sleep(0.2)80        element.send_keys(text)81        self.logger.info(f"输入文本: {text} 到 {locator}")82    
83defswipe_up(self, duration:int=500):84"""向上滑动(默认从底部到顶部的80%)"""85        size = self.driver.get_window_size()86        start_x = size['width']//287        start_y =int(size['height']*0.8)88        end_y =int(size['height']*0.2)89        
90        self.driver.swipe(start_x, start_y, start_x, end_y, duration)91        self.logger.info(f"向上滑动: duration={duration}ms")92    
93defswipe_down(self, duration:int=500):94"""向下滑动"""95        size = self.driver.get_window_size()96        start_x = size['width']//297        start_y =int(size['height']*0.2)98        end_y =int(size['height']*0.8)99        
100        self.driver.swipe(start_x, start_y, start_x, end_y, duration)101    
102defscroll_to_text(self, text:str, max_swipes:int=10):103"""滚动直到找到文本"""104for i inrange(max_swipes):105try:106                self.find_element((AppiumBy.ANDROID_UIAUTOMATOR,f'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(text("{text}"))'))107                self.logger.info(f"滚动找到文本: {text}")108returnTrue109except:110                self.swipe_up()111                time.sleep(0.5)112        
113        self.logger.warning(f"未找到文本: {text}")114returnFalse115    
116# ========== 等待策略 ==========117defwait_for_page_load(self, indicator: Optional[Tuple]=None, timeout:int=15):118"""
119        等待页面加载完成
120        
121        Args:
122            indicator: 页面加载完成的标志元素(可选)
123            timeout: 最大等待时间
124        """125if indicator:126            self.find_element(indicator, timeout)127else:128            time.sleep(2)# 默认等待129        
130        self.logger.info("页面加载完成")131    
132defwait_for_element_disappear(self, locator: Tuple[str,str], timeout:int=10):133"""等待元素消失(用于加载动画)"""134        WebDriverWait(self.driver, timeout).until(135            EC.invisibility_of_element_located(locator)136)137    
138defwait_for_text_present(self, text:str, timeout:int=10):139"""等待文本出现在页面任意位置"""140        WebDriverWait(self.driver, timeout).until(141            EC.presence_of_element_located(142(AppiumBy.ANDROID_UIAUTOMATOR,f'new UiSelector().text("{text}")')143)144)145    
146# ========== 截图与日志 ==========147defsave_screenshot(self, name:str="screenshot"):148"""保存截图到reports/screenshots/"""149import os
150from datetime import datetime
151        
152        screenshot_dir = os.path.join(os.path.dirname(__file__),'..','..','reports','screenshots')153        os.makedirs(screenshot_dir, exist_ok=True)154        
155        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")156        filepath = os.path.join(screenshot_dir,f"{timestamp}_{name}.png")157        self.driver.save_screenshot(filepath)158        self.logger.info(f"截图保存: {filepath}")159return filepath
160    
161def_save_page_source(self, name:str="page_source"):162"""保存页面源码(调试用)"""163import os
164from datetime import datetime
165        
166        source_dir = os.path.join(os.path.dirname(__file__),'..','..','reports','page_sources')167        os.makedirs(source_dir, exist_ok=True)168        
169        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")170        filepath = os.path.join(source_dir,f"{timestamp}_{name}.html")171        
172withopen(filepath,'w', encoding='utf-8')as f:173            f.write(self.driver.page_source)174        
175        self.logger.info(f"页面源码保存: {filepath}")176

📄 pages/base_page.py(业务层)

python
1"""
2Page Object基类(业务层)
3- 继承框架层BasePage
4- 添加业务通用方法
5"""
6from framework.base_page import BasePage
7from appium.webdriver.webdriver import WebDriver
8
9
10class Page(BasePage):
11    """业务页面基类"""
12    
13    # 页面加载标志(子类必须定义)
14    page_loaded_indicator = None
15    
16    def __init__(self, driver: WebDriver):
17        super().__init__(driver)
18        if self.page_loaded_indicator:
19            self.wait_for_page_load(self.page_loaded_indicator)
20    
21    def is_page_loaded(self) -> bool:
22        """检查页面是否加载完成(子类可覆盖)"""
23        if not self.page_loaded_indicator:
24            return True
25        
26        try:
27            self.find_element(self.page_loaded_indicator, timeout=3)
28            return True
29        except:
30            return False
31

📄 pages/login_page.py(具体页面)

python
1"""
2登录页面对象
3"""
4from typing import Tuple
5from pages.base_page import Page
6from appium.webdriver.webdriver import WebDriver
7from appium.webdriver.common.appiumby import AppiumBy
8
9
10class LoginPage(Page):
11    """登录页"""
12    
13    # 页面加载标志
14    page_loaded_indicator = (AppiumBy.ID, "com.example.app:id/et_phone")
15    
16    # 元素定位(从YAML加载,这里写默认值)
17    _locators = {
18        "phone_input": (AppiumBy.ID, "com.example.app:id/et_phone"),
19        "send_code_btn": (AppiumBy.ID, "com.example.app:id/btn_send"),
20        "code_input": (AppiumBy.ID, "com.example.app:id/et_verify_code"),
21        "login_btn": (AppiumBy.ID, "com.example.app:id/btn_login"),
22        "countdown_view": (AppiumBy.ID, "com.example.app:id/tv_countdown"),
23    }
24    
25    def __init__(self, driver: WebDriver):
26        super().__init__(driver)
27    
28    # ========== 业务方法 ==========
29    def input_phone(self, phone: str):
30        """输入手机号"""
31        self.input_text(self._locators["phone_input"], phone)
32        return self  # 支持链式调用
33    
34    def click_send_code(self):
35        """点击发送验证码"""
36        self.click(self._locators["send_code_btn"])
37        # 等待倒计时出现
38        self.wait_for_element_disappear(
39            self._locators["send_code_btn"], 
40            timeout=5
41        )
42        return self
43    
44    def wait_countdown_end(self, max_wait: int = 65):
45        """等待倒计时结束"""
46        self.wait_for_element_disappear(
47            self._locators["countdown_view"],
48            timeout=max_wait
49        )
50        self.logger.info("倒计时已结束")
51        return self
52    
53    def input_code(self, code: str):
54        """输入验证码"""
55        # 先确保输入框可见
56        self.wait_for_element_disappear(self._locators["countdown_view"])
57        self.input_text(self._locators["code_input"], code)
58        return self
59    
60    def click_login(self) -> 'HomePage':
61        """点击登录,返回首页对象"""
62        self.click(self._locators["login_btn"])
63        # 等待跳转
64        time.sleep(2)
65        from pages.home_page import HomePage
66        return HomePage(self.driver)
67    
68    # ========== 完整业务流程 ==========
69    def login_with_phone(self, phone: str, code: str) -> 'HomePage':70"""
71        完整手机号登录流程
72        
73        Args:
74            phone: 手机号
75            code: 验证码
76        
77        Returns:
78            HomePage实例
79        """80        self.logger.info(f"开始登录: {phone}")81        self.input_phone(phone) \
82.click_send_code() \
83.wait_countdown_end() \
84.input_code(code) \
85.click_login()86        
87        self.logger.info("登录成功")88return self  # 实际已返回HomePage89

⚠️ Page Object注意事项:

最佳实践反例
✅ 每个页面一个类 ❌ 一个大类包含所有页面
✅ 业务方法返回self或下一页 ❌ 方法返回None
✅ 定位符集中管理 ❌ 硬编码在方法里
✅ 显式等待(10s) ❌ time.sleep(5)
✅ 失败时自动截图 ❌ 手动截图
✅ 日志记录关键操作 ❌ 无日志

3.3 工具层

📄 utils/logger.py

python
1"""
2统一日志工具
3- 彩色输出
4- 多级别(DEBUG/INFO/ERROR)
5- 自动写入文件
6"""
7import logging
8import os
9from datetime import datetime
10from colorlog import ColoredFormatter
11
12
13class TestLogger:
14    def __init__(self, name: str = "MobileAuto"):
15        self.logger = logging.getLogger(name)
16        self.logger.setLevel(logging.DEBUG)
17        
18        # 控制台handler(彩色)
19        console_handler = logging.StreamHandler()
20        console_handler.setLevel(logging.INFO)
21        console_format = ColoredFormatter(
22            "%(log_color)s[%(asctime)s]%(reset)s %(log_color)s%(levelname)-8s%(reset)s %(message)s",
23            datefmt="%H:%M:%S",
24            log_colors={
25                'DEBUG': 'cyan',
26                'INFO': 'green',
27                'WARNING': 'yellow',
28                'ERROR': 'red',
29                'CRITICAL': 'red,bg_white',
30            }
31        )
32        console_handler.setFormatter(console_format)
33        
34        # 文件handler
35        log_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'logs')
36        os.makedirs(log_dir, exist_ok=True)
37        
38        file_handler = logging.FileHandler(
39            os.path.join(log_dir, f"test_{datetime.now().strftime('%Y%m%d')}.log"),
40            encoding='utf-8'
41        )
42        file_handler.setLevel(logging.DEBUG)
43        file_format = logging.Formatter(
44            "[%(asctime)s] %(levelname)-8s %(name)s: %(message)s",
45            datefmt="%Y-%m-%d %H:%M:%S"
46        )
47        file_handler.setFormatter(file_format)
48        
49        self.logger.addHandler(console_handler)
50        self.logger.addHandler(file_handler)
51    
52    def set_test_name(self, test_name: str):
53        """设置当前测试名称(用于日志)"""
54        self.current_test = test_name
55    
56    def info(self, msg: str):
57        self.logger.info(f"[{self.current_test}] {msg}")
58    
59    def error(self, msg: str):
60        self.logger.error(f"[{self.current_test}] {msg}")
61    
62    def debug(self, msg: str):
63        self.logger.debug(f"[{self.current_test}] {msg}")
64
65
66# 全局实例
67test_logger = TestLogger()
68

📄 utils/screenshot.py

python
1"""
2截图与录屏工具
3- 失败自动截图
4- 支持录屏(可选)
5- 截图自动上传到图床(可选)
6"""
7import os
8import time
9from datetime import datetime
10from typing import Optional
11from appium.webdriver.webdriver import WebDriver
12
13
14class ScreenshotManager:
15    def __init__(self, save_dir: str = "reports/screenshots"):
16        self.save_dir = os.path.join(os.path.dirname(__file__), '..', '..', save_dir)
17        os.makedirs(self.save_dir, exist_ok=True)
18        self.recording = False
19    
20    def take_screenshot(self, driver: WebDriver, name: str = "screenshot") -> str:
21        """
22        截图
23        
24        Args:
25            driver: Appium driver
26            name: 文件名前缀
27        
28        Returns:
29            文件路径
30        """
31        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
32        filepath = os.path.join(self.save_dir, f"{timestamp}_{name}.png")
33        
34        try:
35            driver.save_screenshot(filepath)
36            print(f"✅ 截图保存: {filepath}")
37            return filepath
38        except Exception as e:
39            print(f"❌ 截图失败: {e}")
40            return ""
41    
42    def start_recording(self, driver: WebDriver):
43        """开始录屏(需要Appium 1.18+)"""
44        try:
45            driver.start_recording_screen()
46            self.recording = True
47            print("🎬 开始录屏")
48        except AttributeError:
49            print("⚠️ 当前Appium版本不支持录屏")
50    
51    def stop_recording(self, driver: WebDriver, name: str = "recording") -> str:
52        """停止录屏并保存"""
53        if not self.recording:
54            return ""
55        
56        try:
57            video = driver.stop_recording_screen()
58            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
59            filepath = os.path.join(
60                os.path.dirname(__file__), '..', '..', 
61                'reports', 'videos', 
62                f"{timestamp}_{name}.mp4"
63            )
64            
65            os.makedirs(os.path.dirname(filepath), exist_ok=True)
66            with open(filepath, 'wb') as f:
67                f.write(video)
68            
69            self.recording =False70print(f"🎥 录屏保存: {filepath}")71return filepath
72except Exception as e:73print(f"❌ 录屏失败: {e}")74return""75
76
77# 全局实例78screenshot_manager = ScreenshotManager()79
80
81defauto_screenshot(driver: WebDriver, test_name:str)->str:82"""
83    自动化用例失败时调用的截图函数
84    
85    Args:
86        driver: Appium driver
87        test_name: 用例名称
88    
89    Returns:
90        截图路径
91    """92return screenshot_manager.take_screenshot(driver,f"fail_{test_name.replace('/','_')}")93

📄 utils/adb_helper.py

python
1"""
2ADB命令封装
3- 常用操作封装成Python方法
4- 自动处理设备连接
5"""
6import subprocess
7import time
8from typing import Optional, List
9
10
11class ADBHelper:
12    def __init__(self, device_id: Optional[str] = None):
13        self.device_id = device_id or self._get_default_device()
14    
15    def _get_default_device(self) -> str:
16        """获取默认设备"""
17        result = subprocess.run(
18            ["adb", "devices"], 
19            capture_output=True, 
20            text=True
21        )
22        lines = result.stdout.strip().split('\n')[1:]  # 跳过第一行
23        devices = [line.split('\t')[0] for line in lines if '\tdevice' in line]
24        
25        if not devices:
26            raise ConnectionError("未找到已连接的Android设备")
27        
28        return devices[0]
29    
30    def _run(self, cmd: List[str]) -> str:
31        """执行ADB命令"""
32        full_cmd = ["adb"]
33        if self.device_id:
34            full_cmd.extend(["-s", self.device_id])
35        full_cmd.extend(cmd)
36        
37        result = subprocess.run(full_cmd, capture_output=True, text=True, timeout=30)
38        return result.stdout.strip()
39    
40    # ========== 常用操作 ==========
41    def install_apk(self, apk_path: str):
42        """安装APK"""
43        print(f"📦 安装APK: {apk_path}")
44        self._run(["install", "-r", apk_path])
45        print("✅ 安装成功")
46    
47    def uninstall_app(self, package: str):
48        """卸载应用"""
49        self._run(["uninstall", package])
50    
51    def clear_app_data(self, package: str):
52        """清除应用数据"""
53        self._run(["shell", "pm", "clear", package])
54    
55    def get_current_package(self) -> str:
56        """获取当前前台应用包名"""
57        return self._run(["shell", "dumpsys", "window", "|", "grep", "mCurrentFocus"])
58    
59    def tap(self, x: int, y: int):
60        """点击坐标"""
61        self._run(["shell", "input","tap",str(x),str(y)])62    
63defswipe(self, x1:int, y1:int, x2:int, y2:int, duration:int=300):64"""滑动"""65        self._run(["shell","input","swipe",str(x1),str(y1),str(x2),str(y2),str(duration)])66    
67definput_text(self, text:str):68"""输入文本(需要先focus到输入框)"""69# Android 10+需要先设置输入法70        self._run(["shell","input","text", text])71    
72defpress_key(self, keycode:int):73"""按键"""74        self._run(["shell","input","keyevent",str(keycode)])75    
76defget_screen_size(self)->tuple:77"""获取屏幕尺寸"""78        size = self._run(["shell","wm","size"])79# 输出: Physical size: 1080x240080        parts = size.split()[-1]81        w, h =map(int, parts.split('x'))82return w, h
83    
84deftake_screenshot(self, save_path:str="screenshot.png"):85"""设备截图"""86        self._run(["exec-out","screencap","-p"])87# 实际需要用pull命令88        self._run(["pull","/sdcard/screenshot.png", save_path])89    
90defstart_app(self, package:str, activity:str=None):91"""启动应用"""92if activity:93            self._run(["shell","am","start","-n",f"{package}/{activity}"])94else:95            self._run(["shell","monkey","-p", package,"-c","android.intent.category.LAUNCHER","1"])96    
97defclose_app(self, package:str):98"""关闭应用"""99        self._run(["shell","am","force-stop", package])100    
101defget_logcat(self, clear:bool=True)->str:102"""获取日志"""103if clear:104            self._run(["logcat","-c"])105return self._run(["logcat","-d"])106    
107defwait_for_device(self, timeout:int=30):108"""等待设备连接"""109        start = time.time()110while time.time()- start < timeout:111try:112                self._run(["devices"])113returnTrue114except:115                time.sleep(1)116returnFalse117
118
119# 全局实例(默认设备)120adb = ADBHelper()121

🧪 Phase 4: 用例开发实践

4.1 冒烟测试用例

📄 tests/smoke/test_smoke.py

python
1"""
2冒烟测试 - 核心流程最简验证
3- 每次代码提交后自动执行(5分钟内完成)
4- 失败则阻止合并
5"""
6import pytest
7import allure
8from pages.login_page import LoginPage
9from pages.home_page import HomePage
10
11
12@pytest.mark.smoke
13@pytest.mark.regression
14@allure.feature("冒烟测试")
15class TestSmoke:
16    
17    @allure.story("登录流程")
18    @allure.title("正常手机号登录")
19    def test_login_success(self, driver):
20        """验证登录核心流程"""
21        login_page = LoginPage(driver)
22        
23        # 链式调用
24        home_page = (login_page
25                     .input_phone("13800138001")
26                     .click_send_code()
27                     .wait_countdown_end()
28                     .input_code("123456")
29                     .click_login())
30        
31        # 断言
32        assert home_page.is_page_loaded(), "首页未加载"
33        assert "首页" in home_page.get_page_title()
34    
35    @allure.story("搜索功能")
36    @allure.title("搜索商品")
37    def test_search_product(self, driver):
38        """验证搜索功能"""
39        home_page = HomePage(driver)
40        
41        home_page.search("手机")
42        
43        # 验证搜索结果页出现
44        assert home_page.is_search_result_loaded()
45        assert len(home_page.get_product_list()) > 0
46

4.2 数据驱动用例

📄 config/test_data.yaml

yaml
1login_test_data:
2  - case_id: L001
3    phone: "13800138001"
4    code: "123456"
5    expect: success
6    description: "正常登录"
7    
8  - case_id: L002
9    phone: ""
10    code: "123456"
11    expect: error_phone_empty
12    description: "手机号为空"
13    
14  - case_id: L003
15    phone: "123"
16    code: "123456"
17    expect: error_phone_format
18    description: "手机号格式错误"
19    
20  - case_id: L004
21    phone: "13800138001"
22    code: "000000"
23    expect: error_code_wrong
24    description: "验证码错误"
25
26# 测试数据工厂配置
27data_factory:
28  phone_prefix: "13800138"
29  phone_count: 10
30  code_length: 6
31

📄 utils/data_factory.py

python
1"""
2测试数据工厂
3- 生成随机测试数据
4- 支持从YAML读取
5- 数据隔离(每个用例独立)
6"""
7import random
8import string
9from typing import Any, Dict, List
10from faker import Faker
11import yaml
12
13
14class DataFactory:
15    def __init__(self, config_path: str = "config/test_data.yaml"):
16        self.fake = Faker('zh_CN')
17        self.config = self._load_config(config_path)
18        self._used_phones = set()  # 防止重复
19    
20    def _load_config(self, path: str) -> dict:
21        try:
22            with open(path, 'r', encoding='utf-8') as f:
23                return yaml.safe_load(f)
24        except FileNotFoundError:
25            return {}
26    
27    def generate_phone(self) -> str:
28        """生成随机手机号"""
29        while True:
30            phone = f"1{random.choice(['3', '5', '7', '8', '9'])}{random.randint(100000000, 999999999)}"
31            if phone not in self._used_phones:
32                self._used_phones.add(phone)
33                return phone
34    
35    def generate_code(self, length: int = 6) -> str:
36        """生成随机验证码"""
37        return ''.join(random.choices(string.digits, k=length))
38    
39    def get_test_data(self, test_key: str) -> List[Dict[str, Any]]:
40        """从配置文件读取测试数据"""
41        return self.config.get(test_key, [])
42    
43    def generate_user_data(self) -> dict:
44        """生成完整用户数据"""
45        return {
46            "phone": self.generate_phone(),
47            "code": self.generate_code(),
48            "nickname": self.fake.name(),
49            "address": self.fake.address(),
50        }
51
52
53# 全局实例
54data_factory = DataFactory()
55

📄 tests/login/test_login.py(数据驱动)

python
1"""
2登录模块测试 - 数据驱动
3"""
4import pytest
5import allure
6from pages.login_page import LoginPage
7from utils.data_factory import data_factory
8
9
10@pytest.mark.regression
11@allure.feature("登录模块")
12class TestLogin:
13    
14    @pytest.mark.parametrize("test_data", data_factory.get_test_data("login_test_data"), 
15                           ids=[d["case_id"] for d in data_factory.get_test_data("login_test_data")])
16    @allure.story("登录测试")
17    def test_login_scenarios(self, driver, test_data):
18        """
19        覆盖所有登录场景
20        
21        Args:
22            test_data: 从YAML读取的测试数据
23        """
24        login_page = LoginPage(driver)
25        
26        case_id = test_data["case_id"]
27        phone = test_data["phone"]
28        code = test_data["code"]
29        expect = test_data["expect"]
30        
31        # 根据expect决定操作
32        if expect == "success":
33            login_page.input_phone(phone).click_send_code()
34            login_page.wait_countdown_end().input_code(code)
35            home_page = login_page.click_login()
36            assert home_page.is_page_loaded()
37        
38        elif expect == "error_phone_empty":
39            login_page.input_phone(phone).click_send_code()
40            assert login_page.is_error_message_visible("请输入手机号")
41        
42        # ... 其他场景
43        
44        allure.attach(f"测试数据: {test_data}", name="输入数据", attachment_type=allure.attachment_type.JSON)
45

🚀 Phase 5: CI/CD集成

5.1 GitHub Actions配置

📄 .github/workflows/mobile_test.yml

yaml
1name: 📱 Mobile Automation Tests
2
3on:
4  push:
5    branches: [main, develop]
6  pull_request:
7    branches: [main]
8  schedule:
9    - cron: '0 2 * * *'  # 每天凌晨2点执行
10  workflow_dispatch:  # 手动触发
11    inputs:
12      device:
13        description: '指定设备'
14        required: false
15        default: 'default'
16
17jobs:
18  test:
19    runs-on: macos-14  # macOS运行iOS,如只测Android可用ubuntu-latest
20    
21    strategy:
22      fail-fast: false
23      matrix:
24        python-version: ["3.10"]
25        
26    steps:
27    # ========== 1. 代码检出 ==========
28    - name: Checkout code
29      uses: actions/checkout@v4
30    
31    # ========== 2. 环境准备 ==========
32    - name: Setup Python
33      uses: actions/setup-python@v5
34      with:
35        python-version: ${{ matrix.python-version }}
36    
37    - name: Install dependencies
38      run: |
39        pip install -r requirements.txt
40        pip install appium@2.11.1 pytest-xdist allure-pytest
41    
42    # ========== 3. 启动Appium服务 ==========
43    - name: Start Appium Server
44      run: |
45        appium --allow-insecure chromedriver_autodownload &
46        sleep 10
47        appium-doctor --android  # 检查Android环境
48    
49    # ========== 4. 启动模拟器 ==========
50    - name: Start Android Emulator
51      uses: reactivecircus/android-emulator-runner@v2
52      with:
53        api-level: 34
54        arch: x86_64
55        profile: pixel_6
56        script: |
57          adb wait-for-device
58          adb devices
59    
60    # ========== 5. 执行测试 ==========
61    - name: Run Smoke Tests
62      run: |
63        pytest tests/smoke/ -v --alluredir=reports/allure-results \
64          --html=reports/smoke_report.html --self-contained-html
65      env:
66        DEVICE: ${{ github.event.inputs.device || 'default' }}
67    
68    - name: Run Regression Tests
69      run: |
70        pytest tests/ -v -m "not smoke" --dist loadgroup \
71          --alluredir=reports/allure-results \
72          --html=reports/regression_report.html \
73          --self-contained-html \
74          --count=2  # 并行2个worker
75      env:
76        DEVICE: ${{ github.event.inputs.device || 'default' }}
77    
78    # ========== 6. 生成报告 ==========
79    - name: Generate Allure Report
80      uses: simple-elf/allure-report-action@v1
81      if: always()
82      with:
83        allure_results: reports/allure-results
84        allure_report: reports/allure-report
85        gh_pages: gh-pages  # 发布到GitHub Pages
86    
87    # ========== 7. 通知 ==========
88    - name: Send Notification
89      if: failure()
90      uses: 8398a7/action-slack@v3
91      with:
92        status: ${{ job.status }}
93        text: '移动端自动化测试失败,请查看报告'
94        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
95    
96# ========== 8. 上传日志 ==========97-name: Upload logs
98uses: actions/upload-artifact@v4
99if: always()
100with:101name: test-logs
102path:|103          logs/
104          reports/screenshots/105retention-days:7106

5.2 Jenkins配置(可选)

📄 Jenkinsfile

groovy
1pipeline {
2    agent any
3    
4    parameters {
5        choice(name: 'DEVICE', choices: ['default', 'Pixel6_Android13', 'iPhone15_iOS17'], description: '选择测试设备')
6        booleanParam(name: 'SMOKE_ONLY', defaultValue: false, description: '仅执行冒烟测试')
7    }
8    
9    stages {
10        stage('Checkout') {
11            steps {
12                git branch: 'develop', url: 'git@github.com:your-repo/mobile-auto.git'
13            }
14        }
15        
16        stage('Setup') {
17            steps {
18                sh '''
19                    pip install -r requirements.txt
20                    appium --allow-insecure chromedriver_autodownload &
21                    sleep 5
22                '''
23            }
24        }
25        
26        stage('Test') {
27            parallel {
28                stage('Smoke') {
29                    when { expression { params.SMOKE_ONLY } }
30                    steps {
31                        sh "pytest tests/smoke/ -v --device=${params.DEVICE}"
32                    }
33                }
34                stage('Regression') {
35                    when { expression { !params.SMOKE_ONLY } }
36                    steps {
37                        sh "pytest tests/ -m 'not smoke' -n 2 --device=${params.DEVICE}"
38                    }
39                }
40            }
41        }
42        
43        stage('Report') {
44            always {
45                step([$class: 'AllureReportPublisher', 
46                      reportBuildPolicy: 'ALWAYS',
47                      results: [[path: 'reports/allure-results']]])
48            }
49        }
50    }
51    
52    post {
53        failure {
54            slackSend color: 'danger', 
55                     message: "❌ 移动端测试失败: ${env.BUILD_URL}",
56                     channel: '#mobile-test'
57        }
58    }
59}
60

⚠️ Phase 6: 常见坑点与最佳实践

6.1 元素定位(最高频问题)

问题解决方案优先级
ID经常变化 用accessibility_id(iOS)或content-desc(Android) ⭐⭐⭐⭐⭐
列表元素定位 用UiScrollable + textContains ⭐⭐⭐⭐⭐
动态页面 用page_loaded_indicator等待 ⭐⭐⭐⭐
元素在WebView内 切换context:driver.switch_to.context('WEBVIEW') ⭐⭐⭐⭐
定位失败率>10% 引入AI视觉定位(Airtest/OpenCV) ⭐⭐⭐

代码示例:WebView切换

python
1def switch_to_webview(self):
2    """切换到WebView上下文"""
3    contexts = self.driver.contexts
4    for ctx in contexts:
5        if "WEBVIEW" in ctx or "chrome" in ctx.lower():
6            self.driver.switch_to.context(ctx)
7            self.logger.info(f"切换到WebView: {ctx}")
8            return True
9    return False
10

6.2 等待策略

场景推荐策略代码
页面加载 等待标志元素 WebDriverWait(driver, 15).until(EC.presence_of_element_located(indicator))
加载动画 等待动画消失 WebDriverWait(driver, 10).until(EC.invisibility_of_element_located(loading))
接口返回 轮询+超时 for i in range(30): if check(): break; time.sleep(1)
弹窗 等待可点击 EC.element_to_be_clickable
列表渲染 滚动+等待 scroll_to_text() + presence_of_all_elements_located

反例:

python
1# ❌ 错误:硬编码等待
2time.sleep(5)  # 要么太短(失败),要么太长(浪费时间)
3
4# ✅ 正确:智能等待
5self.wait_for_element_disappear(loading_locator, timeout=15)
6

6.3 测试数据管理

最佳实践说明
每个用例独立数据 用@pytest.fixture生成,避免污染
数据清理放AfterEach 用conftest.py的fixture自动清理
敏感数据不硬编码 从环境变量或Vault读取
账号池管理 用Redis/数据库维护可用账号
Mock替代真实接口 支付/短信走Mock,避免依赖外部服务

6.4 性能监控

python
1# utils/performance.py
2class PerformanceMonitor:
3    def start(self, driver):
4        """开始采集性能数据"""
5        self.driver = driver
6        self.start_time = time.time()
7        
8        # 采集CPU/内存(Android)
9        self.cpu_data = []
10        self.mem_data = []
11    
12    def collect(self):
13        """采集一次"""
14        # CPU
15        cpu = self.driver.execute_script("return android.os.Process.getElapsedCpuTime();")
16        self.cpu_data.append(cpu)
17        
18        # 内存
19        mem = self.driver.execute_script("""
20            Runtime runtime = Runtime.getRuntime();
21            return runtime.totalMemory() - runtime.freeMemory();
22        """)
23        self.mem_data.append(mem / 1024 / 1024)  # MB
24    
25    def get_report(self) -> dict:
26        """生成性能报告"""
27        duration = time.time() - self.start_time
28        return {
29            "duration_s": duration,
30            "cpu_avg_ms": sum(self.cpu_data) / len(self.cpu_data) / 1000000,
31            "mem_peak_mb": max(self.mem_data),
32            "mem_avg_mb": sum(self.mem_data) / len(self.mem_data),
33        }
34

6.5 团队协作规范

规范要求
代码提交 PR必须通过冒烟测试才能合并
用例命名 test_<模块>_<场景>_<预期>,如test_login_phone_success
Page Object 每个页面独立文件,不超过300行
定位符 统一从YAML读取,不硬编码
失败用例 24小时内修复或标记@pytest.mark.skip
文档 每个PR更新docs/目录

📊 演进路线图

 
1Month 1(MVP)          Month 2(稳定化)         Month 3(智能化)
2─────────────────      ─────────────────        ─────────────────
3✅ 框架骨架              ✅ 元素定位自愈           ✅ AI视觉定位
4✅ Page Object           ✅ iOS稳定性提升          ✅ 智能用例生成
5✅ 冒烟测试              ✅ 性能监控               ✅ 失败根因分析
6✅ Allure报告            ✅ 数据工厂               ✅ 智能重试策略
7✅ CI/CD基础             ✅ 插件化架构             ✅ 自然语言用例
8                          ✅ 文档完善               ✅ 跨平台统一
9
10人员:1-2人             人员:2-3人              人员:3-5人
11

🎯 验收清单

框架搭建完成后,逐项检查:

 
1□ 1. 环境验证
2   □ Appium服务正常启动
3   □ Android/iOS设备连接成功
4   □ 运行 hello_world 用例通过
5
6□ 2. 框架核心
7   □ driver_manager 单例正常
8   □ BasePage 等待策略生效
9   □ 日志彩色输出正常
10   □ 失败自动截图生效
11
12□ 3. 用例开发
13   □ 冒烟测试5分钟内完成
14   □ 登录流程用例通过
15   □ 数据驱动正常工作
16   □ 并行执行不冲突
17
18□ 4. CI/CD
19   □ GitHub Actions触发成功
20   □ Allure报告生成
21   □ 失败通知推送
22   □ 产物保留7天
23
24□ 5. 文档
25   □ README可5分钟上手
26   □ 新成员可独立写用例
27   □ 架构图已更新
28   □ 常见问题FAQ
29

💡 终极建议:不要追求一步到位。先用2天搭MVP,跑通1个核心流程,然后每周迭代1个模块。框架是活的,随着用例增长自然演进。

posted on 2026-05-09 12:18  Catonce  阅读(41)  评论(0)    收藏  举报