玩命加载中 . . .

微信小程序自动化测试框架设


微信小程序自动化测试框架设计

基于 Python + pytest + Allure 的全面测试解决方案

功能标签: 功能测试 UI自动化 性能测试 持续集成

引言

核心方案概述

要使用Python+pytest+allure进行微信小程序自动化测试,核心方案是采用微信官方推出的Minium框架作为测试驱动引擎,结合pytest作为灵活的测试执行器,并使用Allure生成美观详尽的测试报告。此组合能够全面覆盖功能、UI和性能测试需求。

环境搭建

安装Python 3.8+、微信开发者工具,并配置好Minium、pytest、allure-pytest等核心库。

框架设计

采用**页面对象模型(Page Object)**设计模式,将页面元素和操作逻辑与测试用例解耦,提高代码可维护性。

测试实现

覆盖功能测试、UI测试和性能测试,生成详细的Allure报告并集成到CI/CD流程。

核心工具选型与技术栈

Minium:微信官方测试框架

Minium 是微信官方团队为开发者量身打造的一套小程序自动化测试框架,它提供了Python和JavaScript两种脚本语言支持。与第三方框架不同,Minium能够深入小程序的内部运行机制,不仅作用于渲染层(UI层面),更能直接干预逻辑层,实现了对小程序更彻底、更全面的测试覆盖 [67]

核心优势

  • 官方支持与深度集成:直接调用小程序底层API,支持对wx对象上的接口进行Mock和调用

  • 跨平台能力:一套脚本可在iOS真机、Android真机以及模拟器上无缝执行

  • 丰富的定位能力:支持WXML选择器、ID选择器、XPath等多种精准定位方式

技术架构

graph TD
A["测试脚本 Python"] --> B["Minium框架"]
B --> C["微信开发者工具"]
C --> D["小程序逻辑层"]
C --> E["小程序渲染层"]
B --> F["iOS/Android真机"]
F --> G["系统原生组件"]

style A fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000
style B fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000
style C fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px,color:#000
style D fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000
style E fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000
style F fill:#fce4ec,stroke:#880e4f,stroke-width:2px,color:#000
style G fill:#f1f8e9,stroke:#33691e,stroke-width:2px,color:#000

Minium vs Appium vs Airtest 框架对比

特性维度 Minium (微信官方) Appium (开源社区) Airtest (网易游戏)
核心原理 基于微信官方测试接口,深入小程序逻辑层和渲染层 [67] 基于WebView技术,将小程序视为Hybrid App测试 [62] 基于图像识别和Poco控件识别,将小程序视为游戏处理 [66]
测试深度 灰盒/白盒测试
可深入逻辑层,操作页面数据,Mock函数和API [67]
黑盒测试
主要操作渲染层(UI),无法直接干预逻辑层
黑盒测试
主要依赖图像匹配和控件属性,无法深入业务逻辑
元素定位 精准且稳定
支持WXML选择器、ID、类名、XPath等 [66]
相对复杂且不稳定
需切换WebView上下文,易受版本影响 [61]
依赖图像识别,不稳定
受屏幕分辨率、UI样式影响大
跨平台性 优秀
一套脚本可在iOS、Android、模拟器上运行 [90]
良好
理论上支持跨平台,但可能需要脚本适配
良好
图像识别天然跨平台,但Poco模式需要适配

测试执行与报告工具

pytest 测试执行器

pytest 是Python生态系统中最流行、最强大的测试框架之一。尽管Minium自带基于unittest的测试基类MiniTest [92],但将Minium与pytest结合使用可以带来更大的灵活性和更丰富的功能。

  • 简洁的断言语句,支持原生Pythonassert

  • 强大的Fixtures机制,管理测试前后操作

  • 丰富的插件支持,如并行执行、失败重试

  • 与Allure的无缝集成 [70]

Allure 测试报告

Allure 是一款轻量级、灵活且支持多语言的测试报告工具。它能够将测试结果以清晰、美观的Web页面形式展示,并提供了丰富的功能来帮助团队更好地理解测试结果。

  • 层次化的报告结构,支持"史诗-特性-故事" [86]

  • 详细的测试步骤,展示执行过程和耗时

  • 丰富的附件支持,如截图、日志、JSON数据

  • 测试用例分类与严重程度标记 [89]

辅助工具

微信开发者工具

微信开发者工具是开发、调试和测试小程序的官方IDE。在自动化测试中扮演着至关重要的角色。

  • 自动化服务端口:提供命令行接口和自动化服务端口,Minium通过该端口与开发者工具通信 [103]

  • 强大调试功能:元素审查(Wxml面板)、网络请求监控(Network面板)、性能分析(Performance面板)

  • 云测服务集成:与微信云测服务(MiniTest)紧密集成 [92]

微信测试包(Android)

对于在Android真机上进行测试,微信官方提供了一个特殊的"微信测试包",内置更多调试和测试接口。

  • 纯净测试环境:确保测试环境的纯净和稳定,避免用户数据干扰

  • 增强测试接口:内置更多调试和测试接口,更好支持Minium自动化操作

  • 版本匹配:需要下载并安装对应版本的微信测试包 [90]

环境搭建与配置

基础环境准备

Python 3.8+ 环境

搭建基于Minium的微信小程序自动化测试环境,首先需要准备一个符合要求的Python环境。根据官方文档和社区实践,Minium框架推荐使用Python 3.8或更高版本 [103] [107]

推荐做法:使用虚拟环境(如venvconda)来隔离项目依赖,确保环境的干净和可复现性。

Node.js 环境

虽然Minium框架本身是基于Python的,但其官方文档的查看和部署却依赖于Node.js环境。Minium的官方文档是使用Docsify这个基于Node.js的文档生成工具编写的 [102]

用途:安装Node.js后,可以通过npm全局安装docsify-cli工具,在本地启动HTTP服务器查阅文档。

Minium框架安装与配置

1. 安装Minium

安装Minium框架是搭建测试环境的核心步骤。Minium提供了便捷的安装方式,可以直接通过pip从官方提供的URL进行安装。

# 安装Minium框架
pip3 install https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip

# 验证安装
python -c "import minium; print(minium.__version__)"

安装命令来源:[103]

2. 配置微信开发者工具

在安装了Minium框架之后,必须对微信开发者工具进行正确的配置,才能使其与Minium协同工作。

开启服务端口

  1. 打开微信开发者工具

  2. 进入"设置" → “安全设置”

  3. 勾选"服务端口"选项 [103] [117]

此操作会开放本地端口(通常是9420),允许外部进程与开发者工具通信

版本要求

  • 开发者工具版本:最新稳定版

  • 基础库版本:不低于2.7.3 [102]

3. 配置测试项目

为了让Minium能够自动启动微信开发者工具并加载指定的小程序项目,需要在测试项目的配置文件中提供正确的路径信息。

config.json 配置文件

{
  "project_path": "D:\\workspace\\my-miniprogram",
  "dev_tool_path": "C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat",
  "debug_mode": "debug"
}

配置说明

  • project_path: 被测小程序项目的根目录路径

  • dev_tool_path: 微信开发者工具命令行工具(cli)的路径

  • debug_mode: 调试模式设置

注意:Windows系统下的路径分隔符应使用双反斜杠\\ [102]

pytest与Allure集成

1. 安装pytest及相关插件

在确定了使用pytest作为测试执行引擎后,首先需要在Python环境中安装pytest及其相关插件。

# 安装pytest
pip install pytest

# 安装常用插件
pip install pytest-xdist        # 并行执行
pip install pytest-html         # HTML报告
pip install pytest-cov          # 代码覆盖率
pip install pytest-rerunfailures # 失败重试

建议将所有依赖记录在项目的requirements.txt文件中

2. 安装allure-pytest插件

allure-pytest插件是实现pytest与Allure报告集成的核心组件。

# 安装allure-pytest插件
pip install allure-pytest

# 验证安装
pytest --help | grep allure

安装成功后,pytest会自动识别并加载该插件。可以通过导入allure模块使用其提供的各种注解和函数。

测试框架详细设计

项目目录结构设计

一个清晰的项目目录结构是框架可维护性的基石。它应该能够直观地反映项目的组成部分,并方便团队成员快速定位代码。以下是一个推荐的目录结构,它融合了pytest的最佳实践和Page Object模式的思想 [37] [42]

wechat_miniprogram_automation/
│
├── config/                      # 配置文件目录
│   ├── __init__.py
│   ├── config.py               # 通用配置
│   ├── dev_config.py           # 开发环境配置
│   ├── test_config.py          # 测试环境配置
│   └── prod_config.py          # 生产环境配置
│
├── data/                        # 测试数据目录
│   ├── __init__.py
│   ├── test_data.yaml          # 通用测试数据
│   └── login_data.json         # 登录模块测试数据
│
├── pages/                       # 页面对象目录
│   ├── __init__.py
│   ├── base_page.py            # 基础页面对象
│   ├── home_page.py            # 首页页面对象
│   ├── login_page.py           # 登录页面对象
│   └── profile_page.py         # 个人中心页面对象
│
├── tests/                       # 测试用例目录
│   ├── __init__.py
│   ├── conftest.py             # pytest的fixture配置文件
│   ├── test_functional/        # 功能测试用例
│   │   ├── __init__.py
│   │   └── test_login.py
│   ├── test_ui/                # UI测试用例
│   │   ├── __init__.py
│   │   └── test_home_ui.py
│   └── test_performance/       # 性能测试用例
│       ├── __init__.py
│       └── test_startup_perf.py
│
├── utils/                       # 工具类目录
│   ├── __init__.py
│   ├── logger.py               # 日志工具
│   ├── driver.py               # 驱动管理工具
│   └── data_reader.py          # 数据读取工具
│
├── reports/                     # 测试报告目录
│   ├── allure-results/         # Allure原始结果文件
│   └── allure-report/          # Allure生成的HTML报告
│
├── requirements.txt             # 项目依赖文件
├── pytest.ini                   # pytest配置文件
└── README.md                    # 项目说明文档

目录结构说明

  • config/: 存放所有配置文件。通过将不同环境(开发、测试、生产)的配置分离,可以方便地在不同环境中切换执行测试。

  • data/: 存放测试数据,支持YAML、JSON等格式。将数据与脚本分离,是实现数据驱动测试(DDT)的关键。

  • pages/: 页面对象模型的核心目录。每个页面对应一个Python类文件,封装了该页面的元素定位和操作逻辑。

  • tests/: 存放所有测试用例。根据测试类型(功能、UI、性能)划分子目录,conftest.py定义共享的fixture。

  • utils/: 存放各种工具类,如日志记录、驱动管理、数据读取等。将通用功能封装成工具类,提高代码复用性。

  • reports/: 存放测试报告。allure-results包含测试原始数据,allure-report是生成的HTML报告。

页面对象模型(Page Object)设计

Page Object Model (POM) 是一种广泛应用于UI自动化测试的设计模式,其核心思想是将页面的UI元素和操作逻辑封装在一个独立的类中,从而实现测试代码与页面实现的解耦。这种设计模式极大地提高了测试脚本的可读性、可维护性和可复用性 [40] [41]

封装页面元素定位

在POM中,每个页面的元素定位器(如ID、XPath、CSS选择器)都被定义为类中的变量。这样做的好处是,如果页面的UI发生变化,只需要修改这一个地方,而无需改动所有引用该元素的测试用例。

示例:pages/login_page.py

from pages.base_page import BasePage
from utils.logger import logger

class LoginPage(BasePage):
    """登录页面对象"""
    
    # 元素定位器
    _USERNAME_INPUT = ("input", "placeholder", "请输入用户名")
    _PASSWORD_INPUT = ("input", "placeholder", "请输入密码")
    _LOGIN_BUTTON = ("button", "inner_text", "登录")
    _ERROR_MESSAGE = ("view", "class", "error-message")

    def __init__(self, mini):
        super().__init__(mini)
        logger.info("初始化登录页面")

设计要点

  • 使用类似(tag, attribute, value)的元组来定义定位器,灵活且易于维护

  • 定位器变量名使用大写加下划线的命名方式,表示常量

  • BasePage中统一解析定位器元组,调用minium的查找方法

封装页面操作逻辑

除了元素定位,页面的所有交互操作(如点击、输入、滑动)也应该被封装成类中的方法。这些方法对外提供清晰的接口,隐藏了内部复杂的实现细节。

    # ... (元素定位器)

    def input_username(self, username):
        """输入用户名"""
        with allure.step(f"输入用户名: {username}"):
            username_element = self.find_element(*self._USERNAME_INPUT)
            username_element.send_keys(username)
            logger.info(f"成功输入用户名: {username}")

    def input_password(self, password):
        """输入密码"""
        with allure.step(f"输入密码: {password}"):
            password_element = self.find_element(*self._PASSWORD_INPUT)
            password_element.send_keys(password)
            logger.info("成功输入密码")

    def click_login_button(self):
        """点击登录按钮"""
        with allure.step("点击登录按钮"):
            login_button = self.find_element(*self._LOGIN_BUTTON)
            login_button.click()
            logger.info("成功点击登录按钮")

    def get_error_message(self):
        """获取错误提示信息"""
        try:
            error_msg_element = self.find_element(*self._ERROR_MESSAGE)
            return error_msg_element.inner_text
        except Exception as e:
            logger.warning(f"未找到错误提示信息: {e}")
            return None

设计要点

  • 每个操作都封装成独立的方法,方法名清晰表达操作意图

  • 使用allure.step装饰器记录操作步骤,增强报告可读性

  • 添加详细的日志记录,便于问题排查

  • 异常处理要合理,避免测试因页面元素不存在而直接失败

实现页面对象与测试用例的解耦

POM的最终目标是实现测试用例与页面实现的完全解耦。测试用例只关心"做什么"(业务逻辑),而不关心"怎么做"(UI交互细节)。当UI发生变化时,只需修改页面对象类,测试用例本身无需任何改动。

示例:tests/test_functional/test_login.py

import allure
import pytest
from pages.login_page import LoginPage
from pages.home_page import HomePage
from data.login_data import login_success_data, login_fail_data

@allure.feature("登录功能")
class TestLogin:
    
    @allure.story("登录成功")
    @pytest.mark.parametrize("data", login_success_data)
    def test_login_success(self, mini, data):
        """测试登录成功场景"""
        login_page = LoginPage(mini)
        login_page.input_username(data["username"])
        login_page.input_password(data["password"])
        login_page.click_login_button()
        
        # 验证是否跳转到首页
        home_page = HomePage(mini)
        assert home_page.is_on_home_page(), "登录失败,未跳转到首页"

    @allure.story("登录失败")
    @pytest.mark.parametrize("data", login_fail_data)
    def test_login_failure(self, mini, data):
        """测试登录失败场景"""
        login_page = LoginPage(mini)
        login_page.input_username(data["username"])
        login_page.input_password(data["password"])
        login_page.click_login_button()
        
        # 验证是否显示错误提示
        error_msg = login_page.get_error_message()
        assert error_msg == data["expected_error"], f"错误提示不符"

设计优势

  • 测试用例中看不到任何minium的API调用,也看不到元素定位器

  • 所有UI交互细节都被封装在页面对象类中

  • 测试用例只关注业务逻辑:输入数据、操作步骤、期望结果

  • UI变化时只需修改页面对象类,测试用例无需改动

测试用例组织与管理

使用pytest的mark标记分类测试

pytestmark功能允许我们为测试用例打上自定义的标签,从而实现灵活的分类和筛选执行。

在pytest.ini中定义标记

[pytest]
markers =
    smoke: 标记冒烟测试用例
    functional: 标记功能测试用例
    ui: 标记UI测试用例
    performance: 标记性能测试用例
    login: 标记登录模块相关用例
    slow: 标记运行较慢的用例

在测试用例中使用标记

@pytest.mark.smoke
@pytest.mark.login
def test_login_success(self, mini):
    # ... 测试代码

@pytest.mark.functional
@pytest.mark.login
def test_login_with_invalid_password(self, mini):
    # ... 测试代码

执行指定标记的用例

# 只运行冒烟测试
pytest -m smoke

# 运行登录模块的功能测试,但排除运行慢的用例
pytest -m "login and functional and not slow"

使用Allure的注解管理测试用例

Allure提供了一套丰富的注解(Annotations),用于在测试报告中展示更详细、更有层次的信息。

常用Allure注解

  • @allure.feature(): 定义功能模块,相当于测试套件

  • @allure.story(): 定义用户故事或功能点

  • @allure.title(): 为测试用例设置友好标题

  • @allure.severity(): 设置用例严重级别

  • @allure.step(): 记录测试步骤

  • @allure.attach(): 在报告中附加文件

综合使用示例

@allure.feature("商品模块")
@allure.story("商品搜索")
class TestProductSearch:
    
    @allure.title("搜索存在的商品")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_search_existing_product(self, mini):
        """测试搜索一个数据库中存在的商品"""
        with allure.step("进入首页"):
            home_page = HomePage(mini)
            home_page.navigate_to_home()
        
        with allure.step("在搜索框输入商品名称"):
            home_page.search_for("iPhone 15")
        
        with allure.step("验证搜索结果"):
            search_result_page = SearchResultPage(mini)
            assert search_result_page.has_product("iPhone 15")
            
        # 附加截图
        allure.attach(mini.capture_screenshot(), 
                     name="搜索结果截图", 
                     attachment_type=allure.attachment_type.PNG)

数据驱动测试(DDT)的实现

数据驱动测试(Data-Driven Testing, DDT)是一种将测试逻辑与测试数据分离的测试方法。通过pytest@pytest.mark.parametrize装饰器,可以非常方便地实现DDT。

测试数据文件

# data/login_data.py
login_test_cases = [
    {
        "title": "使用正确的用户名和密码登录",
        "username": "admin",
        "password": "123456",
        "expected_success": True,
    },
    {
        "title": "使用错误的密码登录",
        "username": "admin",
        "password": "wrong_password",
        "expected_success": False,
        "expected_error": "用户名或密码错误"
    }
]

参数化测试用例

@allure.feature("登录功能")
class TestLogin:
    
    @allure.story("参数化登录测试")
    @pytest.mark.parametrize("case", login_test_cases, 
                           ids=[c["title"] for c in login_test_cases])
    def test_login_with_multiple_data(self, mini, case):
        """使用多组数据测试登录功能"""
        allure.dynamic.title(case["title"])
        
        login_page = LoginPage(mini)
        login_page.input_username(case["username"])
        login_page.input_password(case["password"])
        login_page.click_login_button()
        
        if case["expected_success"]:
            home_page = HomePage(mini)
            assert home_page.is_on_home_page()
        else:
            error_msg = login_page.get_error_message()
            assert error_msg == case["expected_error"]

功能测试实现

基于Minium的API测试

调用小程序内部API

Minium的强大之处在于它不仅能操作UI,还能直接调用小程序逻辑层的API。这使得测试可以深入到业务逻辑内部,验证函数的正确性,而不仅仅是UI的表现。

import allure
import minium

class TestAPI:
    
    @allure.step("调用小程序API: {api_name}")
    def call_wx_api(self, mini, api_name, *args, **kwargs):
        """封装调用wx对象API的方法"""
        try:
            result = mini.app.call_wx_method(api_name, *args, **kwargs)
            allure.attach(str(result), 
                         name=f"{api_name} 返回结果", 
                         attachment_type=allure.attachment_type.TEXT)
            return result
        except Exception as e:
            allure.attach(str(e), 
                         name=f"{api_name} 调用异常", 
                         attachment_type=allure.attachment_type.TEXT)
            raise e
    
    def test_call_custom_function(self, mini):
        """测试调用自定义函数"""
        # 调用小程序中的全局函数
        user_info = self.call_wx_api(mini, "getUserInfo")
        assert user_info is not None, "获取用户信息失败"
        
        # 调用wx.request发起网络请求
        response = self.call_wx_api(mini, "request", {
            "url": "https://api.example.com/data",
            "method": "GET"
        })
        assert response["statusCode"] == 200, "接口请求失败"

关键能力

  • 通过mini.app.call_wx_method直接调用小程序API

  • 可以调用自定义的全局函数,如getUserInfo

  • 可以调用wx.request等原生API发起网络请求

  • 使用Allure附件记录API调用结果,便于问题排查

模拟用户操作流程

功能测试的核心是模拟真实的用户操作流程,从用户的角度验证整个业务流程的正确性。这通常涉及到多个页面的跳转和一系列连续的UI操作。

import allure
from pages.home_page import HomePage
from pages.product_list_page import ProductListPage
from pages.product_detail_page import ProductDetailPage
from pages.cart_page import CartPage

@allure.feature("购物车功能")
class TestShoppingCart:
    
    @allure.story("添加商品到购物车")
    def test_add_product_to_cart(self, mini):
        """测试完整的添加商品到购物车流程"""
        with allure.step("1. 进入首页"):
            home_page = HomePage(mini)
            home_page.navigate_to_home()
        
        with allure.step("2. 进入商品列表页"):
            home_page.click_product_list_tab()
            product_list_page = ProductListPage(mini)
        
        with allure.step("3. 选择第一个商品进入详情页"):
            product_list_page.click_first_product()
            product_detail_page = ProductDetailPage(mini)
        
        with allure.step("4. 点击加入购物车"):
            product_detail_page.click_add_to_cart_button()
        
        with allure.step("5. 进入购物车页面验证"):
            product_detail_page.go_to_cart()
            cart_page = CartPage(mini)
            assert cart_page.has_product(), "购物车中没有商品"

测试流程

  1. 首页导航:验证应用启动后正确加载首页

  2. 商品列表:从首页进入商品列表页

  3. 商品详情:选择商品进入详情页面

  4. 添加到购物车:在详情页点击加入购物车按钮

  5. 购物车验证:进入购物车确认商品已添加

验证业务逻辑正确性

在模拟操作流程后,需要对关键的业务逻辑进行验证。这不仅包括UI上的变化(如页面跳转、元素显示),更重要的是验证数据层面的正确性。

    # ... 接上文 test_add_product_to_cart
    
    with allure.step("6. 验证购物车数据"):
        # 获取购物车页面的商品数量(UI显示)
        product_count_in_ui = cart_page.get_product_count()
        
        # 通过Minium直接获取小程序购物车数据(逻辑层)
        cart_data = mini.app.call_wx_method("getCartData")
        product_count_in_data = len(cart_data.get("items", []))
        
        # 验证UI显示与数据一致
        assert product_count_in_ui == product_count_in_data, \
            f"购物车商品数量不一致,UI显示: {product_count_in_ui}, 数据: {product_count_in_data}"
        
        # 验证商品信息正确
        first_product = cart_data["items"][0]
        assert "id" in first_product, "商品缺少ID"
        assert "name" in first_product, "商品缺少名称"
        assert "price" in first_product, "商品缺少价格"
        
        allure.attach(str(cart_data), 
                     name="购物车数据", 
                     attachment_type=allure.attachment_type.JSON)

验证策略

  • UI与数据一致性验证:比较UI显示的商品数量与逻辑层数据是否一致

  • 数据结构完整性验证:检查商品数据是否包含必要的字段(ID、名称、价格)

  • 业务规则验证:验证商品价格计算、库存检查等业务规则

  • 附件记录:将购物车数据作为JSON附件添加到报告中

网络请求与响应验证

拦截与Mock网络请求

在功能测试中,经常需要测试在不同网络响应下的应用表现,例如接口返回错误、超时或空数据。Minium支持拦截和Mock网络请求,使得这类测试变得简单。

import allure
from unittest import mock

@allure.feature("网络异常处理")
class TestNetworkErrorHandling:
    
    @allure.story("Mock接口返回500错误")
    def test_handle_server_error(self, mini):
        """测试当后端接口返回500错误时,前端的处理逻辑"""
        
        # 定义Mock的响应
        mock_response = {
            "statusCode": 500,
            "data": {"message": "Internal Server Error"}
        }
        
        # 使用mock.patch拦截wx.request调用
        with mock.patch.object(mini.app, 'call_wx_method', 
                              side_effect=lambda method, *args, **kwargs: 
                              mock_response if method == "request" else None):
            
            with allure.step("触发需要调用接口的操作"):
                # 执行会触发网络请求的操作
                # ...
                
            with allure.step("验证前端是否显示友好的错误提示"):
                # 断言错误提示是否正确显示
                error_message = self.get_error_message()
                assert "服务器繁忙" in error_message, \
                    "未显示友好的错误提示"

Mock技术要点

  • 使用unittest.mock模块拦截API调用

  • 定义Mock响应数据,模拟各种异常场景

  • 验证前端在异常情况下的处理逻辑

  • 确保应用能够显示友好的错误提示

验证接口返回数据

除了Mock,还可以直接验证真实接口返回的数据是否符合预期,包括数据结构、字段完整性、数据准确性等。

@allure.feature("API接口测试")
class TestAPIResponse:
    
    @allure.story("验证商品详情接口数据")
    def test_product_detail_api_response(self, mini):
        """测试商品详情接口返回的数据结构是否正确"""
        
        with allure.step("调用商品详情接口"):
            product_id = "12345"
            response = mini.app.call_wx_method("request", {
                "url": f"https://api.example.com/products/{product_id}",
                "method": "GET"
            })
            
        with allure.step("验证接口返回数据"):
            # 附加原始响应数据
            allure.attach(str(response), 
                         name="接口原始返回", 
                         attachment_type=allure.attachment_type.JSON)
            
            # 验证HTTP状态码
            assert response["statusCode"] == 200, "接口请求失败"
            
            # 验证数据结构
            data = response["data"]
            assert "id" in data, "缺少商品ID字段"
            assert "name" in data, "缺少商品名称字段"
            assert "price" in data, "缺少商品价格字段"
            assert data["id"] == product_id, "商品ID不匹配"
            
            # 验证数据类型
            assert isinstance(data["price"], (int, float)), "价格类型不正确"
            assert data["price"] > 0, "价格必须大于0"

验证要点

  • HTTP状态码验证:确保接口请求成功

  • 数据结构验证:检查必要字段的存在性

  • 数据准确性验证:确保返回的数据与请求参数匹配

  • 数据类型验证:确保字段类型符合预期

  • 业务规则验证:如价格必须大于0等

数据驱动测试

使用外部数据源

将测试数据存储在外部文件(如JSON或YAML)中,可以实现测试逻辑与数据的完全分离,便于管理和维护。

data/search_test_data.json

[
  {
    "keyword": "iPhone",
    "expected_count": 5,
    "expected_category": "电子产品"
  },
  {
    "keyword": "华为",
    "expected_count": 3,
    "expected_category": "电子产品"
  },
  {
    "keyword": "不存在的商品",
    "expected_count": 0,
    "expected_message": "未找到相关商品"
  }
]

data_reader.py工具类

import json
import yaml
import allure

class DataReader:
    """数据读取工具类"""
    
    @staticmethod
    def read_json(file_path):
        """读取JSON文件"""
        with open(file_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        return data
    
    @staticmethod
    def read_yaml(file_path):
        """读取YAML文件"""
        with open(file_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
        return data

实现参数化测试用例

使用pytest@pytest.mark.parametrize装饰器,可以轻松地读取外部数据文件,并将其作为参数传递给测试用例。

import allure
import pytest
from pages.home_page import HomePage
from utils.data_reader import DataReader

def load_search_data():
    """加载搜索测试数据"""
    return DataReader.read_json("data/search_test_data.json")

@allure.feature("搜索功能")
class TestSearch:
    
    @allure.story("参数化搜索测试")
    @pytest.mark.parametrize("data", load_search_data())
    def test_search_with_different_keywords(self, mini, data):
        """使用不同关键词进行搜索,并验证结果"""
        keyword = data["keyword"]
        
        with allure.step(f"搜索关键词: {keyword}"):
            home_page = HomePage(mini)
            home_page.search_for(keyword)
            
        with allure.step("验证搜索结果"):
            actual_count = home_page.get_search_result_count()
            
            if data["expected_count"] > 0:
                assert actual_count == data["expected_count"], \
                    f"搜索结果数量不符,期望: {data['expected_count']}, 实际: {actual_count}"
                
                # 验证搜索结果分类
                categories = home_page.get_search_result_categories()
                assert data["expected_category"] in categories, \
                    f"未找到期望的分类: {data['expected_category']}"
            else:
                # 验证无结果时的提示
                message = home_page.get_no_result_message()
                assert message == data["expected_message"], \
                    f"无结果提示不符,期望: {data['expected_message']}, 实际: {message}"

实现要点

  • load_search_data函数负责加载外部数据文件

  • 使用@pytest.mark.parametrize将数据传递给测试用例

  • 根据数据动态设置Allure用例标题

  • 支持多种验证场景:有结果、无结果、特定分类等

  • 数据与测试逻辑完全分离,便于维护和扩展

UI自动化测试实现

元素定位策略

基于WXML的控件识别

Minium最强大的特性之一是能够直接根据小程序的WXML结构来定位元素,这与Web自动化中的DOM定位非常相似,使得定位更加精准和稳定。

使用ID、类选择器、标签选择器

Minium支持使用类似CSS选择器的语法来定位元素,这是最常用也是最推荐的定位方式。

# 通过ID定位(唯一标识)
element = page.get_element("#submit-button")

# 通过类名定位(可能返回多个元素)
elements = page.get_elements(".product-item")

# 通过标签名定位
input_elements = page.get_elements("input")

# 组合选择器
submit_btn = page.get_element("button#submit-button.primary-btn")

最佳实践

  • 优先使用ID选择器,确保唯一性
  • 类选择器适合定位一组相似的元素
  • 标签选择器适合定位特定类型的控件
  • 组合选择器可以提高定位的精确性

使用XPath定位

对于复杂的定位场景,可以使用XPath。虽然功能强大,但XPath的可读性和性能通常不如CSS选择器,应谨慎使用。

# 定位具有特定文本内容的元素
element = page.get_element('//*[contains(text(), "立即购买")]')

# 定位父元素下的第一个子元素
element = page.get_element('//view[@class="product-list"]/view[1]')

# 使用属性定位
element = page.get_element('//input[@placeholder="请输入手机号"]')

# 使用逻辑运算符
element = page.get_element('//button[@class="submit" and @disabled="false"]')

使用建议

  • 尽量避免使用绝对路径的XPath
  • 优先使用属性定位而非索引定位
  • 复杂的XPath表达式应添加详细注释
  • 考虑XPath的可维护性和性能影响

元素定位最佳实践

推荐做法

  • 与开发团队约定稳定的元素标识符

  • 优先使用语义化的ID和类名

  • 封装复杂的定位逻辑到页面对象中

  • 为动态元素添加稳定的测试属性

避免做法

  • 使用绝对路径的XPath表达式

  • 依赖文本内容进行定位(多语言问题)

  • 使用易变的属性(如style)进行定位

  • 在测试代码中硬编码定位表达式

高级技巧

  • 使用CSS选择器组合提高定位精度

  • 实现智能等待机制处理异步加载

  • 封装常用的定位模式为辅助方法

  • 使用相对定位减少耦合度

用户交互模拟

基本交互操作

Minium提供了丰富的API来模拟用户的各种交互行为,包括点击、输入、滑动等。

点击操作

# 点击元素
element.click()

# 长按元素(500毫秒)
element.long_press(duration=500)

# 双击元素
element.double_click()

# 点击坐标
mini.app.click(100, 200)

输入操作

# 在输入框中输入文本
input_element.send_keys("test input")

# 清空输入框
input_element.clear()

# 模拟键盘事件
mini.app.press_key("Enter")

# 输入特殊字符
input_element.send_keys("\n")  # 换行

滑动操作

# 从一个坐标滑动到另一个坐标
mini.app.swipe(100, 500, 100, 200, duration=500)

# 在元素上滑动
element.scroll_to(0, 500)

# 滚动到页面底部
mini.app.page_scroll_to_bottom()

# 下拉刷新
mini.app.pull_down_refresh()

处理原生组件

小程序中经常会遇到系统原生的组件,如授权弹窗、地图等。Minium通过minium.Native类提供了对这些原生组件的操作能力。

处理授权弹窗

def handle_permission_popup(self, mini):
    """处理授权弹窗"""
    try:
        # 查找"允许"按钮
        allow_button = mini.native.get_element_by_text("允许")
        if allow_button:
            allow_button.click()
            return True
            
        # 查找"拒绝"按钮
        deny_button = mini.native.get_element_by_text("拒绝")
        if deny_button:
            deny_button.click()
            return False
            
    except Exception as e:
        print(f"未找到授权弹窗或处理失败: {e}")
    return False

处理日期选择器

def select_date(self, mini, year, month, day):
    """选择日期"""
    # 打开日期选择器
    date_picker = self.find_element(*self._DATE_PICKER)
    date_picker.click()
    
    # 等待日期选择器出现
    mini.wait_for_element("picker-view", max_timeout=3)
    
    # 选择年、月、日
    mini.native.scroll_to(year, "picker-view-column", index=0)
    mini.native.scroll_to(month, "picker-view-column", index=1)
    mini.native.scroll_to(day, "picker-view-column", index=2)
    
    # 点击确定
    confirm_button = mini.native.get_element_by_text("确定")
    confirm_button.click()

处理系统对话框

def handle_system_dialog(self, mini, action="confirm"):
    """处理系统对话框"""
    try:
        if action == "confirm":
            # 点击确定
            confirm_btn = mini.native.get_element_by_text("确定")
            confirm_btn.click()
        elif action == "cancel":
            # 点击取消
            cancel_btn = mini.native.get_element_by_text("取消")
            cancel_btn.click()
        return True
    except Exception as e:
        print(f"处理系统对话框失败: {e}")
        return False

UI状态与数据校验

验证页面元素属性

UI测试的一个重要环节是验证元素的属性是否符合预期,如文本内容、是否可见、是否可点击等。

class TestUIValidation:
    
    def test_element_attributes(self, mini):
        """验证元素属性"""
        # 获取元素的文本
        text = element.inner_text
        assert text == "预期文本", f"文本不匹配,期望: 预期文本,实际: {text}"
        
        # 判断元素是否可见
        is_visible = element.is_displayed()
        assert is_visible is True, "元素应该可见"
        
        # 判断元素是否启用
        is_enabled = not element.get_attribute("disabled")
        assert is_enabled is True, "元素应该可用"
        
        # 判断元素是否存在
        is_exists = page.element_is_exists("#some-element")
        assert is_exists is True, "元素应该存在"
        
        # 获取元素的样式属性
        color = element.value_of_css_property("color")
        assert color == "rgb(255, 0, 0)", "颜色不匹配"
        
        # 获取元素的尺寸和位置
        rect = element.rect
        assert rect["width"] > 0, "元素宽度应该大于0"
        assert rect["height"] > 0, "元素高度应该大于0"

验证维度

  • 文本内容验证:检查元素显示的文本是否符合预期

  • 可见性验证:确保元素在页面上可见

  • 可用性验证:检查元素是否可交互(未被禁用)

  • 存在性验证:确认元素存在于DOM中

  • 样式验证:检查元素的CSS属性,如颜色、字体等

  • 布局验证:检查元素的尺寸和位置

验证页面数据展示

除了元素属性,还需要验证页面上展示的数据是否与逻辑层的数据一致,确保UI正确反映了应用的状态。

class TestDataDisplay:
    
    def test_product_price_display(self, mini):
        """验证商品价格显示正确"""
        # 获取页面上显示的商品价格
        price_element = page.get_element(".product-price")
        price_text = price_element.inner_text
        displayed_price = float(price_text.replace("¥", "").replace(",", ""))
        
        # 通过Minium获取逻辑层中该商品的价格
        product_data = mini.app.call_wx_method("getCurrentProductData")
        actual_price = product_data["price"]
        
        # 验证两者是否一致
        assert displayed_price == actual_price, \
            f"价格显示不一致,页面显示: {displayed_price}, 实际数据: {actual_price}"
    
    def test_cart_items_display(self, mini):
        """验证购物车商品列表显示正确"""
        # 获取页面上显示的商品列表
        ui_items = []
        item_elements = page.get_elements(".cart-item")
        for element in item_elements:
            name = element.get_element(".item-name").inner_text
            quantity = int(element.get_element(".item-quantity").inner_text)
            ui_items.append({"name": name, "quantity": quantity})
        
        # 获取逻辑层中的购物车数据
        cart_data = mini.app.call_wx_method("getCartData")
        data_items = [{"name": item["name"], "quantity": item["quantity"]} 
                     for item in cart_data["items"]]
        
        # 验证UI显示与数据一致
        assert len(ui_items) == len(data_items), "商品数量不一致"
        
        for ui_item, data_item in zip(ui_items, data_items):
            assert ui_item["name"] == data_item["name"], "商品名称不一致"
            assert ui_item["quantity"] == data_item["quantity"], "商品数量不一致"
    
    def test_form_data_binding(self, mini):
        """验证表单数据双向绑定"""
        # 在表单中输入数据
        form_page = FormPage(mini)
        test_data = {
            "name": "张三",
            "phone": "13800138000",
            "email": "zhangsan@example.com"
        }
        
        form_page.fill_form(test_data)
        
        # 通过Minium获取表单数据
        form_data = mini.app.call_wx_method("getFormData")
        
        # 验证数据绑定正确
        for key, value in test_data.items():
            assert form_data[key] == value, f"字段 {key} 数据绑定不正确"

验证策略

  • 数据一致性验证:比较UI显示的数据与逻辑层数据是否一致

  • 列表完整性验证:确保所有数据项都正确显示,无遗漏或重复

  • 数据绑定验证:检查表单等双向绑定组件的数据同步

  • 数据转换验证:确保数据格式转换正确(如价格格式化)

  • 实时更新验证:验证数据变化时UI能够及时更新

性能测试实现

性能数据采集

启动性能数据

小程序的启动性能是用户体验的关键指标。可以通过Minium配合微信开发者工具或云测服务,采集从点击小程序图标到首屏渲染完成的各个阶段耗时。

关键指标

  • 冷启动时间

  • 热启动时间

  • 首屏渲染时间

  • 页面可交互时间

# 示例:获取启动时间
startup_time = mini.app.get_performance_metric("startupTime")
allure.attach(f"启动时间: {startup_time}ms", 
             name="启动性能数据")

运行时性能

在测试执行过程中,可以定时采集小程序的CPU和内存占用情况,以发现内存泄漏或性能瓶颈。

监控指标

  • CPU使用率

  • 内存占用

  • FPS帧率

  • 网络请求耗时

# 示例:采集运行时性能
cpu_usage = mini.app.get_cpu_usage()
memory_usage = mini.app.get_memory_usage()
allure.attach(f"CPU: {cpu_usage}%, 内存: {memory_usage}MB", 
             name="运行时性能")

云测服务集成

微信官方的云测服务提供了更专业的性能测试能力,可以在真机环境中进行全面的性能分析。

服务特性

  • 真机性能测试

  • 性能基线对比

  • 详细性能报告

  • 历史趋势分析

# 示例:上传性能数据到云测
mini.cloud.upload_performance_data(
    startup_time=startup_time,
    cpu_usage=cpu_usage,
    memory_usage=memory_usage
)

使用Minium接口获取性能数据

虽然Minium没有直接暴露获取性能数据的API,但可以通过执行自定义的JavaScript代码来获取部分性能信息,或者通过分析微信开发者工具的日志文件来间接获取。

import allure
import time
import psutil

class PerformanceCollector:
    """性能数据收集器"""
    
    def __init__(self, mini):
        self.mini = mini
        self.performance_data = {}
    
    @allure.step("采集运行时性能数据")
    def collect_runtime_performance(self, duration=10):
        """在指定时间内采集CPU和内存数据"""
        cpu_data = []
        memory_data = []
        
        for i in range(duration):
            try:
                # 获取小程序进程的CPU和内存使用率
                # 这里需要根据实际情况获取小程序进程的PID
                # 以下是一个示例实现
                
                # 执行JavaScript获取性能信息
                perf_info = self.mini.app.evaluate("""
                    (function() {
                        var perf = wx.getPerformance();
                        return perf.now();
                    })()
                """)
                
                # 模拟CPU和内存数据(实际实现需要获取真实进程信息)
                cpu_percent = 15.2  # 模拟CPU使用率
                memory_mb = 180.5   # 模拟内存占用
                
                cpu_data.append(cpu_percent)
                memory_data.append(memory_mb)
                
                allure.attach(f"第{i+1}秒: CPU {cpu_percent}%, 内存 {memory_mb}MB", 
                             name=f"性能数据-{i+1}", 
                             attachment_type=allure.attachment_type.TEXT)
                
                time.sleep(1)
            except Exception as e:
                print(f"采集性能数据失败: {e}")
                break
        
        # 计算统计数据
        if cpu_data and memory_data:
            avg_cpu = sum(cpu_data) / len(cpu_data)
            max_memory = max(memory_data)
            
            summary = f"平均CPU: {avg_cpu:.2f}%, 峰值内存: {max_memory:.2f}MB"
            self.performance_data["runtime"] = {
                "avg_cpu": avg_cpu,
                "max_memory": max_memory,
                "cpu_data": cpu_data,
                "memory_data": memory_data
            }
            
            allure.attach(summary, 
                         name="性能数据摘要", 
                         attachment_type=allure.attachment_type.TEXT)
            
            return self.performance_data["runtime"]
        
        return None
    
    @allure.step("采集页面加载性能")
    def collect_page_load_performance(self):
        """采集页面加载性能数据"""
        # 执行JavaScript获取性能指标
        load_metrics = self.mini.app.evaluate("""
            (function() {
                var timing = performance.timing;
                return {
                    page_load_time: timing.loadEventEnd - timing.navigationStart,
                    first_paint: timing.responseStart - timing.navigationStart,
                    dom_ready: timing.domContentLoadedEventEnd - timing.navigationStart
                };
            })()
        """)
        
        if load_metrics:
            self.performance_data["page_load"] = load_metrics
            
            for metric, value in load_metrics.items():
                allure.attach(f"{metric}: {value}ms", 
                             name=f"页面加载性能-{metric}", 
                             attachment_type=allure.attachment_type.TEXT)
            
            return load_metrics
        
        return None
    
    def get_performance_report(self):
        """生成性能报告"""
        report = {}
        if "runtime" in self.performance_data:
            runtime = self.performance_data["runtime"]
            report["runtime_performance"] = {
                "average_cpu_usage": f"{runtime['avg_cpu']:.2f}%",
                "peak_memory_usage": f"{runtime['max_memory']:.2f}MB",
                "duration_seconds": len(runtime["cpu_data"])
            }
        
        if "page_load" in self.performance_data:
            report["page_load_performance"] = self.performance_data["page_load"]
        
        return report

技术要点

  • 使用evaluate方法执行自定义JavaScript获取性能信息

  • 封装PerformanceCollector类统一管理性能数据采集

  • 支持运行时性能监控和页面加载性能分析

  • 将性能数据作为Allure附件添加到测试报告

  • 生成结构化的性能报告,便于分析和比较

性能测试用例设计与分析

关键业务路径性能测试

针对用户最常使用的核心业务流程(如登录、下单、支付)设计性能测试用例,监控这些流程的耗时和资源消耗。

@allure.feature("性能测试")
class TestCriticalPathPerformance:
    
    @allure.story("登录流程性能")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_login_performance(self, mini):
        """测试登录流程的性能"""
        login_page = LoginPage(mini)
        performance_collector = PerformanceCollector(mini)
        
        # 开始性能监控
        performance_collector.start_monitoring()
        
        with allure.step("执行登录操作"):
            # 测量登录操作的耗时
            start_time = time.time()
            login_page.input_username("test_user")
            login_page.input_password("test_password")
            login_page.click_login_button()
            
            # 等待登录完成
            home_page = HomePage(mini)
            home_page.wait_for_home_page()
            
            end_time = time.time()
            login_duration = (end_time - start_time) * 1000  # 转换为毫秒
            
            allure.attach(f"登录耗时: {login_duration:.2f}ms", 
                         name="登录性能指标", 
                         attachment_type=allure.attachment_type.TEXT)
        
        # 收集性能数据
        performance_data = performance_collector.stop_monitoring()
        
        # 验证性能指标
        assert login_duration < 3000, "登录耗时超过3秒"
        assert performance_data["max_memory"] < 200, "内存占用超过200MB"
        assert performance_data["avg_cpu"] < 30, "CPU使用率超过30%"
        
        # 将性能数据添加到报告
        performance_collector.attach_performance_data()

测试策略

  • 核心流程覆盖:选择用户最常用的关键业务流程进行测试

  • 多维度监控:同时监控时间、CPU、内存等多个性能指标

  • 基线对比:与历史性能数据或基准值进行比较

  • 阈值验证:设置性能指标的合理阈值,超出时触发告警

  • 报告集成:将性能数据集成到测试报告中,便于分析

页面渲染性能测试

测试复杂页面(如长列表、大量图片)的渲染性能,验证滚动是否流畅,是否存在掉帧现象。

@allure.feature("性能测试")
class TestRenderingPerformance:
    
    @allure.story("长列表滚动性能")
    @allure.severity(allure.severity_level.NORMAL)
    def test_long_list_scroll_performance(self, mini):
        """测试长列表滚动性能"""
        # 进入商品列表页
        product_list_page = ProductListPage(mini)
        product_list_page.navigate_to_list()
        
        # 等待页面加载完成
        mini.wait_for_element(".product-item", max_timeout=10)
        
        # 开始性能监控
        performance_collector = PerformanceCollector(mini)
        performance_collector.start_monitoring()
        
        with allure.step("执行滚动操作"):
            # 滚动到页面底部
            start_time = time.time()
            mini.app.page_scroll_to_bottom()
            
            # 滚动回顶部
            mini.app.page_scroll_to_top()
            end_time = time.time()
            
            scroll_duration = (end_time - start_time) * 1000
            allure.attach(f"滚动耗时: {scroll_duration:.2f}ms", 
                         name="滚动性能指标", 
                         attachment_type=allure.attachment_type.TEXT)
        
        # 停止监控并收集数据
        performance_data = performance_collector.stop_monitoring()
        
        # 分析滚动性能
        fps_data = self.analyze_fps(performance_data)
        avg_fps = sum(fps_data) / len(fps_data) if fps_data else 0
        
        allure.attach(f"平均FPS: {avg_fps:.2f}", 
                     name="帧率分析", 
                     attachment_type=allure.attachment_type.TEXT)
        
        # 验证性能指标
        assert scroll_duration < 2000, "滚动耗时超过2秒"
        assert avg_fps > 30, "平均帧率低于30FPS"
        assert performance_data["max_memory"] < 300, "内存占用超过300MB"
        
        # 检查是否有掉帧现象
        frame_drops = sum(1 for fps in fps_data if fps < 30)
        frame_drop_rate = frame_drops / len(fps_data) if fps_data else 0
        
        allure.attach(f"掉帧率: {frame_drop_rate:.2%}", 
                     name="掉帧分析", 
                     attachment_type=allure.attachment_type.TEXT)
        
        assert frame_drop_rate < 0.1, "掉帧率超过10%"
    
    def analyze_fps(self, performance_data):
        """分析FPS数据"""
        # 基于CPU和内存数据估算FPS
        # 实际实现可能需要更复杂的算法或专用工具
        fps_data = []
        for i in range(len(performance_data["cpu_data"])):
            cpu = performance_data["cpu_data"][i]
            # 简单的FPS估算:CPU使用率越低,FPS越高
            estimated_fps = max(60 - int(cpu), 1)
            fps_data.append(estimated_fps)
        return fps_data

测试重点

  • 滚动流畅度:验证页面滚动是否平滑,无卡顿

  • 帧率稳定性:监控FPS是否保持在合理范围内

  • 内存管理:检查内存占用是否合理,无内存泄漏

  • 掉帧检测:识别和量化掉帧现象

  • 多场景覆盖:测试不同复杂度页面的渲染性能

性能结果分析与报告

将采集到的性能数据(如启动耗时、内存峰值)作为附件添加到Allure报告中,与功能测试结果一同展示,形成全面的质量视图。

class PerformanceReporter:
    """性能报告生成器"""
    
    def __init__(self, performance_data):
        self.performance_data = performance_data
        self.baseline = self.load_baseline()
    
    def load_baseline(self):
        """加载性能基线数据"""
        try:
            with open("config/performance_baseline.json", "r") as f:
                return json.load(f)
        except FileNotFoundError:
            return {}
    
    def generate_performance_report(self):
        """生成性能报告"""
        report = {}
        
        # 比较当前性能与基线
        for metric, current_value in self.performance_data.items():
            baseline_value = self.baseline.get(metric, {})
            
            if isinstance(current_value, (int, float)):
                baseline = baseline_value.get("value", 0)
                threshold = baseline_value.get("threshold", 0.1)
                
                change = (current_value - baseline) / baseline if baseline else 0
                status = "normal" if abs(change) <= threshold else "warning"
                
                report[metric] = {
                    "current": current_value,
                    "baseline": baseline,
                    "change": change,
                    "status": status,
                    "unit": baseline_value.get("unit", "")
                }
        
        return report
    
    def attach_performance_report(self):
        """将性能报告附加到Allure"""
        report = self.generate_performance_report()
        
        # 生成HTML格式的报告
        html_report = self.generate_html_report(report)
        
        allure.attach(html_report, 
                     name="性能分析报告", 
                     attachment_type=allure.attachment_type.HTML)
        
        # 检查是否有性能退化
        has_performance_degradation = any(
            item["status"] == "warning" for item in report.values()
        )
        
        if has_performance_degradation:
            allure.attach("检测到性能退化,请查看详细报告", 
                         name="性能告警", 
                         attachment_type=allure.attachment_type.TEXT)
    
    def generate_html_report(self, report):
        """生成HTML格式的性能报告"""
        html = "<h2>性能分析报告</h2>"
        html += "<table border='1'><tbody><tr><th>指标</th><th>当前值</th><th>基线值</th><th>变化</th><th>状态</th></tr>"
        
        for metric, data in report.items():
            status_color = "green" if data["status"] == "normal" else "red"
            change_color = "green" if data["change"] <= 0 else "red"
            
            html += f"<tr><td>{metric}</td><td>{data['current']}{data['unit']}</td><td>{data['baseline']}{data['unit']}</td><td style='color: {change_color}'>{data['change']:+.2%}</td><td style='color: {status_color}'>{data['status']}</td></tr>"
        
        html += "</tbody></table>"
        return html
    
    def check_performance_alerts(self):
        """检查性能告警"""
        report = self.generate_performance_report()
        alerts = []
        
        for metric, data in report.items():
            if data["status"] == "warning":
                alerts.append({
                    "metric": metric,
                    "current": data["current"],
                    "baseline": data["baseline"],
                    "change": data["change"]
                })
        
        return alerts

# 在测试用例中使用
def test_performance_with_reporting(self, mini):
    """带报告的性能测试"""
    # 执行性能测试并收集数据
    performance_collector = PerformanceCollector(mini)
    performance_data = performance_collector.collect_runtime_performance(duration=15)
    
    if performance_data:
        # 生成性能报告
        reporter = PerformanceReporter(performance_data)
        reporter.attach_performance_report()
        
        # 检查性能告警
        alerts = reporter.check_performance_alerts()
        if alerts:
            # 在CI/CD中触发告警
            self.trigger_performance_alerts(alerts)

def trigger_performance_alerts(self, alerts):
    """触发性能告警"""
    for alert in alerts:
        message = (f"性能告警: {alert['metric']} 当前值 {alert['current']} "
                  f"超出基线 {alert['baseline']} ({alert['change']:+.2%})")
        
        # 可以在CI/CD中发送通知
        # send_notification(message)
        allure.attach(message, 
                     name="性能告警", 
                     attachment_type=allure.attachment_type.TEXT)
        
        # 也可以将告警信息写入日志或发送到监控系统
        logger.warning(message)

报告功能

  • 基线对比:与历史性能数据进行比较,识别性能变化

  • 阈值检测:自动检测超出阈值的性能指标

  • 可视化报告:生成HTML格式的性能分析报告

  • 告警机制:在性能退化时触发告警通知

  • CI/CD集成:将性能测试集成到持续集成流程中

测试执行与报告生成

本地测试执行

使用pytest命令行执行测试

在本地开发或调试时,可以直接使用pytest命令来执行测试,通过不同的参数控制测试的执行方式。

基本执行命令

# 执行所有测试
pytest

# 执行指定目录下的测试
pytest tests/test_functional/

# 执行指定标记的测试
pytest -m smoke

# 生成Allure原始结果
pytest --alluredir=./reports/allure-results

# 并行执行测试(加速执行)
pytest -n auto

# 失败重试
pytest --reruns 3

执行参数说明

  • -v: 显示详细信息

  • --tb=short: 使用简短的错误回溯

  • -m: 执行指定标记的测试

  • -n: 并行执行测试

  • --reruns: 失败重试次数

使用配置文件管理执行参数

为了避免每次都输入一长串命令行参数,可以在pytest.ini文件中配置默认参数。

pytest.ini 配置示例

[pytest]
# 默认命令行参数
addopts = -v --tb=short --alluredir=./reports/allure-results

# 测试路径
testpaths = tests

# 标记定义
markers =
    smoke: 冒烟测试
    functional: 功能测试
    ui: UI测试
    performance: 性能测试
    login: 登录模块测试
    slow: 运行较慢的测试

# 插件配置
norecursedirs = .* venv src

# 测试超时设置
timeout = 300

# 并行执行配置
workers = auto

# 失败重试配置
reruns = 2
reruns_delay = 1

配置优势

  • 简化测试执行命令,提高开发效率

  • 统一团队测试执行标准

  • 集中管理测试配置和标记

  • 便于CI/CD环境配置

云端测试服务(可选)

微信云测服务介绍

微信官方提供了云测服务(MiniTest),可以在云端的大量真机上执行自动化测试,解决了本地设备不足和兼容性问题。

核心优势

  • 海量真机设备

  • 多版本兼容性测试

  • 自动化测试执行

  • 详细测试报告

# 云测服务配置
cloud_config = {
    "project_id": "your-project-id",
    "test_plan": "your-test-plan",
    "devices": ["ios-14", "android-11"]
}

上传用例与创建测试计划

在云测平台上,可以上传打包好的测试用例,并创建测试计划,指定要运行的设备、测试类型等。

测试计划配置

  • 选择测试用例

  • 配置测试设备

  • 设置执行参数

  • 定时执行策略

# 创建测试计划
def create_test_plan():
    return {
        "name": "每日回归测试",
        "test_cases": ["test_login", "test_checkout"],
        "devices": ["iPhone 12", "Pixel 4"],
        "schedule": "daily"
    }

查看云端测试报告

测试执行完毕后,可以在云测平台上查看详细的测试报告,包括性能分析、兼容性分析、日志、截图等。

报告内容

  • 测试概览统计

  • 设备兼容性结果

  • 性能分析报告

  • 错误日志和截图

# 获取测试报告
def get_cloud_test_report(test_id):
    # 通过API获取云测报告
    response = requests.get(
        f"https://cloud.weixin.qq.com/api/test-report/{test_id}"
    )
    return response.json()

云测服务集成示例

class CloudTestService:
    """微信云测服务集成"""
    
    def __init__(self, project_id, api_key):
        self.project_id = project_id
        self.api_key = api_key
        self.base_url = "https://cloud.weixin.qq.com/api"
    
    def upload_test_package(self, test_package_path):
        """上传测试包"""
        url = f"{self.base_url}/project/{self.project_id}/upload"
        
        with open(test_package_path, "rb") as f:
            files = {"file": f}
            response = requests.post(url, 
                                   headers={"Authorization": f"Bearer {self.api_key}"},
                                   files=files)
            
            if response.status_code == 200:
                return response.json().get("test_package_id")
            else:
                raise Exception(f"上传失败: {response.text}")
    
    def create_test_plan(self, name, test_package_id, devices, schedule=None):
        """创建测试计划"""
        url = f"{self.base_url}/project/{self.project_id}/test-plan"
        
        data = {
            "name": name,
            "test_package_id": test_package_id,
            "devices": devices,
            "schedule": schedule or {"type": "manual"}
        }
        
        response = requests.post(url,
                               headers={"Authorization": f"Bearer {self.api_key}"},
                               json=data)
        
        if response.status_code == 200:
            return response.json().get("test_plan_id")
        else:
            raise Exception(f"创建测试计划失败: {response.text}")
    
    def execute_test_plan(self, test_plan_id):
        """执行测试计划"""
        url = f"{self.base_url}/test-plan/{test_plan_id}/execute"
        
        response = requests.post(url,
                               headers={"Authorization": f"Bearer {self.api_key}"})
        
        if response.status_code == 200:
            return response.json().get("test_execution_id")
        else:
            raise Exception(f"执行测试计划失败: {response.text}")
    
    def get_test_results(self, test_execution_id):
        """获取测试结果"""
        url = f"{self.base_url}/test-execution/{test_execution_id}/results"
        
        response = requests.get(url,
                               headers={"Authorization": f"Bearer {self.api_key}"})
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"获取测试结果失败: {response.text}")
    
    def download_test_report(self, test_execution_id, output_path):
        """下载测试报告"""
        url = f"{self.base_url}/test-execution/{test_execution_id}/report"
        
        response = requests.get(url,
                               headers={"Authorization": f"Bearer {self.api_key}"},
                               stream=True)
        
        if response.status_code == 200:
            with open(output_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            return True
        else:
            raise Exception(f"下载测试报告失败: {response.text}")

# 使用示例
def run_cloud_test():
    """执行云测"""
    cloud_service = CloudTestService(
        project_id="your-project-id",
        api_key="your-api-key"
    )
    
    # 1. 上传测试包
    test_package_id = cloud_service.upload_test_package("dist/test_package.zip")
    print(f"测试包ID: {test_package_id}")
    
    # 2. 创建测试计划
    test_plan_id = cloud_service.create_test_plan(
        name="每日回归测试",
        test_package_id=test_package_id,
        devices=[
            {"type": "ios", "model": "iPhone 12"},
            {"type": "android", "model": "Pixel 4"}
        ],
        schedule={
            "type": "daily",
            "time": "02:00"
        }
    )
    print(f"测试计划ID: {test_plan_id}")
    
    # 3. 执行测试计划
    test_execution_id = cloud_service.execute_test_plan(test_plan_id)
    print(f"测试执行ID: {test_execution_id}")
    
    # 4. 等待测试完成并获取结果
    # 实际应用中应该轮询检查测试状态
    time.sleep(3600)  # 等待测试完成
    
    results = cloud_service.get_test_results(test_execution_id)
    print(f"测试结果: {results}")
    
    # 5. 下载测试报告
    report_path = f"reports/cloud_test_report_{test_execution_id}.zip"
    cloud_service.download_test_report(test_execution_id, report_path)
    print(f"测试报告已下载: {report_path}")
    
    return test_execution_id, results

集成要点

  • 封装CloudTestService类管理云测API调用

  • 支持完整的测试生命周期:上传、创建、执行、获取结果

  • 提供错误处理和重试机制

  • 可以将云测结果集成到本地测试报告中

  • 支持定时执行和手动触发两种模式

Allure报告生成与查看

生成Allure原始结果文件

pytest执行时,通过--alluredir参数指定的目录会生成包含测试原始数据的JSON文件。这些文件是生成最终HTML报告的基础。

执行测试并生成结果

# 执行测试并生成Allure结果
pytest --alluredir=./reports/allure-results

# 使用其他常用参数
pytest -v --tb=short --reruns 2 \
  --alluredir=./reports/allure-results \
  -m "not slow"

结果文件结构

reports/
└── allure-results/
    ├── test-cases/
    │   ├── test_login_success.json
    │   ├── test_login_failure.json
    │   └── ...
    ├── attachments/
    │   ├── screenshot-1.png
    │   ├── log-1.txt
    │   └── ...
    ├── categories.json
    ├── environment.json
    ├── executor.json
    └── history.json

结果文件说明

  • test-cases/: 包含每个测试用例的执行结果

  • attachments/: 存储测试过程中附加的文件(截图、日志等)

  • categories.json: 测试分类配置

  • environment.json: 测试环境信息

  • executor.json: 执行器信息

  • history.json: 历史执行数据(用于趋势分析)

生成并查看HTML格式报告

使用Allure命令行工具将原始结果文件渲染成美观的HTML报告,支持多种查看和分析功能。

Allure命令行操作

# 生成HTML报告
allure generate ./reports/allure-results -o ./reports/allure-report --clean

# 在浏览器中打开报告
allure open ./reports/allure-report

# 直接生成并打开报告(简化命令)
allure serve ./reports/allure-results

# 添加历史数据(用于趋势分析)
copy /history/* ./reports/allure-results/

报告查看功能

  • 层次化结构:按"史诗-特性-故事"组织测试用例

  • 详细步骤展示:清晰展示测试执行过程和耗时

  • 丰富附件支持:查看截图、日志、性能数据等

  • 灵活筛选功能:按严重程度、状态等筛选结果

  • 趋势分析:查看历史执行数据和趋势

报告结构说明

reports/
└── allure-report/
    ├── index.html          # 报告入口
    ├── data/
    │   ├── test-cases.json # 测试用例数据
    │   ├── categories.json # 分类数据
    │   └── history.json    # 历史数据
    ├── plugins/
    │   └── ...             # 插件文件
    ├── styles/
    │   └── ...             # 样式文件
    └── images/
        └── ...             # 图片资源

Allure报告最佳实践

报告优化建议

  • 使用@allure.title为测试用例设置友好标题

  • 使用@allure.step记录关键操作步骤

  • 在失败时自动截图并附加到报告

  • 使用@allure.severity标记用例优先级

  • 附加环境信息和配置文件

高级配置

环境信息配置

# environment.json
{
  "OS": "Windows 10",
  "Python": "3.8.10",
  "Minium": "1.0.0",
  "pytest": "7.0.0",
  "微信开发者工具": "1.05.2204250"
}

测试分类配置

# categories.json
[
  {
    "name": "产品缺陷",
    "matchedStatuses": ["failed"]
  },
  {
    "name": "测试缺陷",
    "matchedStatuses": ["broken"]
  },
  {
    "name": "性能问题",
    "messageRegex": ".*性能.*",
    "matchedStatuses": ["failed"]
  }
]

持续集成(CI/CD)集成

Jenkins集成

将自动化测试流程集成到Jenkins流水线中,实现代码提交后自动触发测试,及时发现问题。

集成步骤

  • 配置Jenkins Pipeline

  • 设置Git webhook触发

  • 配置测试环境

  • 执行测试脚本

  • 生成测试报告

GitLab CI集成

使用GitLab CI/CD配置自动化测试流程,与代码仓库紧密集成,实现持续测试。

配置要点

  • .gitlab-ci.yml配置

  • Docker环境构建

  • 测试阶段定义

  • 报告生成和发布

  • 合并请求集成

GitHub Actions

利用GitHub Actions自动化测试工作流,与GitHub仓库无缝集成,支持多种触发方式。

工作流配置

  • workflow.yml定义

  • 触发条件设置

  • 测试环境准备

  • 结果通知配置

  • 状态徽章展示

Jenkins Pipeline集成示例

以下是一个完整的Jenkins Pipeline配置示例,展示了如何将微信小程序自动化测试集成到CI/CD流程中。

pipeline {
    agent {
        docker {
            image 'python:3.8-slim'
            args '-u root'
        }
    }
    
    environment {
        PROJECT_PATH = 'wechat_miniprogram_automation'
        ALLURE_RESULTS = 'reports/allure-results'
        ALLURE_REPORT = 'reports/allure-report'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Setup Environment') {
            steps {
                echo '设置测试环境...'
                
                // 安装Node.js(用于微信开发者工具)
                sh 'curl -sL https://deb.nodesource.com/setup_14.x | bash -'
                sh 'apt-get install -y nodejs'
                
                // 安装Python依赖
                sh 'pip install --upgrade pip'
                sh 'pip install -r requirements.txt'
                
                // 安装Allure命令行工具
                sh '''
                    apt-get update && apt-get install -y wget unzip
                    wget https://github.com/allure-framework/allure2/releases/download/2.13.8/allure-2.13.8.zip
                    unzip allure-2.13.8.zip -d /opt/
                    ln -s /opt/allure-2.13.8/bin/allure /usr/bin/allure
                '''
                
                // 安装微信开发者工具
                sh '''
                    wget https://servicewechat.com/wxa-dev-tools/download/main
                    tar -xzf main -C /opt/
                '''
            }
        }
        
        stage('Run Tests') {
            steps {
                echo '执行自动化测试...'
                
                // 执行测试并生成Allure结果
                sh '''
                    pytest -v --alluredir=${ALLURE_RESULTS} \
                    --reruns 2 --reruns-delay 1
                '''
            }
            
            post {
                always {
                    // 归档测试结果
                    archiveArtifacts artifacts: "${ALLURE_RESULTS}/**", 
                                   allowEmptyArchive: true
                }
            }
        }
        
        stage('Generate Report') {
            steps {
                echo '生成Allure测试报告...'
                
                // 生成HTML报告
                sh 'allure generate ${ALLURE_RESULTS} -o ${ALLURE_REPORT} --clean'
                
                // 归档测试报告
                archiveArtifacts artifacts: "${ALLURE_REPORT}/**", 
                               allowEmptyArchive: true
            }
        }
        
        stage('Notify Results') {
            steps {
                echo '发送测试通知...'
                
                // 获取测试统计
                script {
                    def testResults = currentBuild.rawBuild.getTestResultAction()
                    def totalTests = testResults ? testResults.totalCount : 0
                    def failedTests = testResults ? testResults.failCount : 0
                    def skippedTests = testResults ? testResults.skipCount : 0
                    
                    def message = """
                    *微信小程序自动化测试完成*
                    
                    总用例数: ${totalTests}
                    失败数: ${failedTests}
                    跳过数: ${skippedTests}
                    通过率: ${totalTests > 0 ? (totalTests - failedTests) / totalTests * 100 : 0}%
                    
                    构建: ${env.BUILD_URL}
                    报告: ${env.BUILD_URL}/allure
                    """
                    
                    // 发送通知到企业微信/钉钉
                    // 根据测试结果发送不同级别的通知
                    if (failedTests > 0) {
                        // 发送失败通知
                        dingtalkSend message: message, 
                                   robot: 'test-notification'
                    } else {
                        // 发送成功通知
                        dingtalkSend message: message, 
                                   robot: 'test-notification'
                    }
                }
            }
        }
    }
    
    post {
        always {
            // 清理工作空间
            cleanWs()
        }
        
        success {
            echo '测试执行成功!'
        }
        
        failure {
            echo '测试执行失败!'
        }
        
        unstable {
            echo '测试结果不稳定!'
        }
    }
}

// 辅助函数
def dingtalkSend(Map params) {
    // 实际实现应调用钉钉API发送通知
    echo "发送通知: ${params.message}"
}

自动化触发测试任务

在CI/CD工具中配置自动化触发条件,当代码仓库有新的提交时,自动拉取代码,安装依赖,并执行测试命令。

常见触发条件

  • 代码提交(push)触发

  • 合并请求(pull request)触发

  • 定时执行(如每日构建)

  • 标签发布(release)触发

触发配置示例

# GitHub Actions触发配置
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * *'  # 每天凌晨2点
  workflow_dispatch:  # 手动触发

测试结果通知与反馈

测试执行完毕后,可以将Allure报告的链接通过邮件、企业微信、Slack等方式发送给团队成员,实现快速反馈。

通知方式

  • 企业微信/钉钉:通过群机器人发送测试结果

  • 邮件通知:发送详细的测试报告邮件

  • Slack通知:在Slack频道中发送测试更新

  • Webhook通知:调用自定义Webhook发送通知

通知内容模板

# 企业微信通知模板
{
  "msgtype": "markdown",
  "markdown": {
    "content": """## 自动化测试完成

    **状态**: ${status}
    **通过率**: ${pass_rate}%
    **总用例**: ${total_tests}
    **失败**: ${failed_tests}
    **跳过**: ${skipped_tests}
    
    [查看详细报告](${report_url})
    
    > 构建: ${build_url}
    > 分支: ${branch}
    > 提交: ${commit}"""
  }
}

CI/CD集成最佳实践

优化建议

  • 使用Docker容器确保环境一致性

  • 缓存依赖包加速构建过程

  • 并行执行测试缩短执行时间

  • 设置合理的测试超时时间

  • 定期清理旧的构建和报告

注意事项

  • 保护敏感信息(API密钥、密码等)

  • 处理测试环境的依赖和配置

  • 设置适当的资源限制

  • 监控CI/CD管道的性能和稳定性

  • 建立回滚机制处理测试失败

关于本文档

本文档详细介绍了基于Python + pytest + Allure的微信小程序自动化测试框架的设计与实现,涵盖了功能测试、UI测试和性能测试的全面解决方案。

核心工具

  • Minium - 微信官方测试框架

  • pytest - 灵活的测试执行器

  • Allure - 美观的测试报告

  • 微信开发者工具 - 调试与测试

相关资源

  • Minium官方文档

  • pytest官方文档

  • Allure官方文档

  • 微信开发者工具


文章作者: Gavin Wang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Gavin Wang !
  目录