如何高效编写Airtest用例,如何可持续地维护用例脚本

告别“一锅粥”脚本:Airtest用例高效编写与可持续维护的最佳实践

Airtest以其“所见即所得”的图像识别和简洁的API,极大地降低了UI自动化的门槛。然而,“写得爽”不等于“维护得好”。一个缺乏良好设计的自动化项目,最终会变成一个难以维护、频繁失败且无人敢动的“代码山”。

本文将提供一套从“入门”到“架构”的实践指南,帮助你构建一个高效、健壮且易于维护的Airtest自动化测试体系。

第一部分:高效编写 —— 杜绝坏味道,从源头提升质量

效率不仅仅是写得快,更是写得对、写得稳。

1. 告别sleep(),拥抱wait()exists()

这是新手最容易犯的错误。滥用sleep()会导致用例执行时间无故拉长,并且在网络波动或设备性能差异时极易失败。

  • 坏味道 (Bad Smell):
touch(Template(r"tpl16.../login_btn.png"))
sleep(5.0) # 等待首页加载,但5秒可能不够,也可能太长
touch(Template(r"tpl16.../home_icon.png"))
  • 最佳实践 (Best Practice): 使用wait()exists()进行智能等待,直到目标元素出现。
touch(Template(r"tpl16.../login_btn.png"))
# 等待首页标志性图标出现,最长等待20秒,超时则抛出异常
wait(Template(r"tpl16.../home_icon.png"), timeout=20) 
touch(Template(r"tpl16.../home_icon.png"))

# 或者用exists()做判断
if exists(Template(r"tpl16.../home_icon.png")):
    print("登录成功,进入首页")
else:
    raise AssertionError("登录失败,未找到首页图标")

优势:用例更健壮,执行效率更高,只在需要时等待。

2. Poco优先,图像为辅

如果你的应用(或游戏)接入了Poco-SDK,请优先使用Poco进行元素定位

  • 图像识别的痛点:受分辨率、主题、动画、同名图标等影响,不够稳定。
  • Poco的优势:直接获取UI控件树,定位精准、稳定,不受视觉变化影响,且执行速度远快于图像识别。
  • 低效且不稳
touch(Template(r"tpl16.../submit_button.png")) 
  • 高效且稳定
# 使用Poco的UI路径定位
poco("com.app.package:id/submit_button").click()
# 或者通过文本定位
poco(text="提交").click()

原则:能用Poco,绝不用图像。只有在Poco无法定位的自定义控件、Canvas绘制内容、或验证UI渲染正确性时,才使用图像识别作为补充。

3. assert断言是测试的灵魂

自动化脚本如果只有操作而没有检查点,那就不是“测试用例”,而是“操作流程”。assert语句是验证结果是否符合预期的关键。

  • 无效用例
# ...执行一系列操作...
print("流程执行完毕") # 鬼知道中间对没对
  • 有效用例
# ...执行登录操作...
# 断言:检查登录后是否出现了“我的账户”这个文本
assert_exists(Template(r"tpl16.../my_account.png"), "登录后应出现'我的账户'页面")

# 使用Poco断言
assert poco(text="欢迎回来,张三").exists(), "欢迎语不正确"

核心:每个关键步骤后都应有断言,确保上一步操作达到了预期结果。


第二部分:可持续维护 —— 告别“面条代码”,拥抱“工程化”

当用例数量超过10个,你就必须考虑维护性问题了。核心思想是分层、封装、解耦

1. 核心思想:页面对象模型(Page Object Model, POM)

这是UI自动化领域的黄金标准,同样适用于Airtest。

  • 理念:将UI界面的操作和业务流程的测试代码分离开。每个页面对应一个类,类中封装了该页面的元素定位和操作方法。
  • 目的
    • 高复用性:登录操作在很多用例中都会用到,只需在LoginPage类中写一次。
    • 易维护性:如果登录按钮的UI变了,只需修改LoginPage类中的一个定位方法,所有调用它的用例都能自动更新,无需逐一修改。
    • 可读性强:测试用例代码变得非常清晰,如同在描述业务流程。

示例:重构一个登录用例

  • 重构前(面条式代码):
# test_login.air
from airtest.core.api import *
auto_setup(__file__)

touch(Template(r"tpl_username_field.png"))
text("my_user")
touch(Template(r"tpl_password_field.png"))
text("my_password")
touch(Template(r"tpl_login_btn.png"))
assert_exists(Template(r"tpl_welcome_msg.png"), "登录成功断言")
  • 重构后(POM模式):
    1. 创建页面对象层 (page_objects/login_page.py):
# page_objects/login_page.py
from airtest.core.api import *
from poco.drivers.android.api import AndroidUIPoco

class LoginPage:
    def __init__(self, poco: AndroidUIPoco):
        self.poco = poco
        # 将元素定位信息集中管理
        self.username_field = self.poco("id/username")
        self.password_field = self.poco("id/password")
        self.login_btn = self.poco("id/login_button")

    def login(self, username, password):
        self.username_field.set_text(username)
        self.password_field.set_text(password)
        self.login_btn.click()
    1. 创建页面对象层 (page_objects/home_page.py):
# page_objects/home_page.py
class HomePage:
    def __init__(self, poco: AndroidUIPoco):
        self.poco = poco
        self.welcome_message = self.poco(textMatches="欢迎.*")

    def is_login_successful(self):
        return self.welcome_message.exists()
    1. 编写清晰的测试用例层 (testcases/test_login.py):
# testcases/test_login.py
from airtest.core.api import *
from poco.drivers.android.api import AndroidUIPoco
from page_objects.login_page import LoginPage
from page_objects.home_page import HomePage

# 初始化Poco和页面对象
poco = AndroidUIPoco(use_airtest_input=True, screenshot_each_action=False)
login_page = LoginPage(poco)
home_page = HomePage(poco)

# 测试用例主体
def test_successful_login():
    login_page.login("my_user", "my_password")
    assert home_page.is_login_successful(), "登录失败,未找到欢迎信息"

# 运行测试
if __name__ == '__main__':
    auto_setup(__file__)
    test_successful_login()
2. 数据驱动(Data-Driven Testing)

将测试数据(如用户名、密码、期望结果)与测试逻辑分离。这样,你可以用同一套脚本验证多种数据场景。

  • 实现方式:使用YAML, CSV, Excel等文件存储测试数据。
  • 示例 (data/login_data.csv):
username,password,expected
correct_user,correct_pass,True
wrong_user,correct_pass,False
correct_user,wrong_pass,False
  • 用例中读取数据并循环测试:
# test_login_data_driven.py
import csv

# ... import and setup ...

def run_login_test(username, password, expected_success):
    login_page.login(username, password)
    if expected_success:
        assert home_page.is_login_successful(), f"用户{username}本应登录成功"
    else:
        # 假设有错误提示
        assert poco(text="用户名或密码错误").exists(), f"用户{username}登录失败的提示不正确"
    # ... 可能还需要登出或返回操作 ...

# 读取CSV并执行
with open('data/login_data.csv', 'r') as f:
    reader = csv.DictReader(f)
    for row in reader:
        run_login_test(row['username'], row['password'], row['expected'] == 'True')
3. 统一配置管理

将易变的信息,如设备ID、应用包名、服务器地址等,提取到配置文件中,而不是硬编码在脚本里。

  • 示例 (config.ini):
[Android]
device = android://127.0.0.1:5037/serial_number
package_name = com.example.app

[User]
default_user = testuser
  • 在基础脚本中读取:
import configparser
from airtest.core.api import *

config = configparser.ConfigParser()
config.read('config.ini')

device_str = config.get('Android', 'device')
package_name = config.get('Android', 'package_name')

# 使用配置连接设备
connect_device(device_str)
start_app(package_name)

总结:打造你的自动化测试框架

一个健壮的Airtest项目,其目录结构可能如下所示:

my_airtest_project/
├── testcases/                # 测试用例层
│   ├── test_login.py
│   └── test_shopping_cart.py
├── page_objects/             # 页面对象层
│   ├── base_page.py          # (可选)页面基类,封装通用方法
│   ├── login_page.py
│   └── home_page.py
├── data/                     # 数据层
│   └── login_data.csv
├── reports/                  # 测试报告输出目录
├── utils/                    # 公共工具类
│   └── helper.py             # (如自定义log、截图等)
├── config.ini                # 配置文件
└── run_all_tests.py          # 测试执行入口脚本

高效编写与维护的终极清单:

  1. 【高效】 优先使用wait/exists替代sleep
  2. 【高效】 优先使用Poco定位替代图像识别。
  3. 【高效】 必须使用assert进行结果验证。
  4. 【维护】 采用**页面对象模型(POM)**分离UI操作与测试逻辑。
  5. 【维护】 采用数据驱动分离测试数据与测试脚本。
  6. 【维护】 采用配置文件管理环境信息。
  7. 【维护】 建立清晰的项目结构,各司其职。

遵循以上实践,你的Airtest项目将从一个脆弱、混乱的“手工作坊”演变为一个稳健、高效、易于扩展的“自动化工厂”,真正实现自动化测试的价值。

你可能感兴趣的:(深入浅出自动化测试,Airtest)