Airtest以其“所见即所得”的图像识别和简洁的API,极大地降低了UI自动化的门槛。然而,“写得爽”不等于“维护得好”。一个缺乏良好设计的自动化项目,最终会变成一个难以维护、频繁失败且无人敢动的“代码山”。
本文将提供一套从“入门”到“架构”的实践指南,帮助你构建一个高效、健壮且易于维护的Airtest自动化测试体系。
效率不仅仅是写得快,更是写得对、写得稳。
sleep()
,拥抱wait()
和exists()
这是新手最容易犯的错误。滥用sleep()
会导致用例执行时间无故拉长,并且在网络波动或设备性能差异时极易失败。
touch(Template(r"tpl16.../login_btn.png"))
sleep(5.0) # 等待首页加载,但5秒可能不够,也可能太长
touch(Template(r"tpl16.../home_icon.png"))
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("登录失败,未找到首页图标")
优势:用例更健壮,执行效率更高,只在需要时等待。
如果你的应用(或游戏)接入了Poco-SDK,请优先使用Poco进行元素定位。
touch(Template(r"tpl16.../submit_button.png"))
# 使用Poco的UI路径定位
poco("com.app.package:id/submit_button").click()
# 或者通过文本定位
poco(text="提交").click()
原则:能用Poco,绝不用图像。只有在Poco无法定位的自定义控件、Canvas绘制内容、或验证UI渲染正确性时,才使用图像识别作为补充。
assert
断言是测试的灵魂自动化脚本如果只有操作而没有检查点,那就不是“测试用例”,而是“操作流程”。assert
语句是验证结果是否符合预期的关键。
# ...执行一系列操作...
print("流程执行完毕") # 鬼知道中间对没对
# ...执行登录操作...
# 断言:检查登录后是否出现了“我的账户”这个文本
assert_exists(Template(r"tpl16.../my_account.png"), "登录后应出现'我的账户'页面")
# 使用Poco断言
assert poco(text="欢迎回来,张三").exists(), "欢迎语不正确"
核心:每个关键步骤后都应有断言,确保上一步操作达到了预期结果。
当用例数量超过10个,你就必须考虑维护性问题了。核心思想是分层、封装、解耦。
这是UI自动化领域的黄金标准,同样适用于Airtest。
LoginPage
类中写一次。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"), "登录成功断言")
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()
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()
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()
将测试数据(如用户名、密码、期望结果)与测试逻辑分离。这样,你可以用同一套脚本验证多种数据场景。
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')
将易变的信息,如设备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 # 测试执行入口脚本
高效编写与维护的终极清单:
wait/exists
替代sleep
。Poco
定位替代图像识别。assert
进行结果验证。遵循以上实践,你的Airtest项目将从一个脆弱、混乱的“手工作坊”演变为一个稳健、高效、易于扩展的“自动化工厂”,真正实现自动化测试的价值。