1.背景

1.1 作者背景

作者自从 2020 年中有段时间发了几篇文章之后,一直忙于工作,没有机会发文,如今赋闲在家,得以发表一些测试心得。

作者在过去一年多内,在某大型游戏公司负责游戏 SDK 业务的质量保障工作,在客户端自动化测试方面有一定的探索和实践。

1.2 项目背景

1.2.1 为什么要做客户端自动化测试

相信这个问题的答案,大家心里都有答案。有人说是:发现 bug,验证功能;有人说是:降低回归测试成本;有人说是:提高测试能力;不一而足。

对于 SDK 业务而言,无论是登录还是支付,稳定性要求都非常高,不允许出现失误。软件的变动(包括客户端和服务端)是否会影响功能,如果能在客户端层面进行自动化的确认,不论对质量,还是效率,都有很重要的意义。

除了测试能力对开发服务开放以外,我们还需要对运维服务。

自 2020 年以来,世界经济都在下行,各家公司整体都在降本增效,以应对经济下行的压力。与此同时,微服务和多中间件架构已经成为了各家公司的标配,这些中间件和服务器平时实际使用率并不高,存在优化的空间。公司倾向于缩减硬件成本,运维也有 KPI 要求。对于测试人员的影响就是:服务端和中间件的硬件变动需要一定的回归测试来验证。在我们团队中,这种情况不是一次两次,而是在整个降本周期内一直存在。

1.2.2 为什么能做客户端自动化测试

任何一个决策,都应该考虑可行性,客户端自动化测试也不例外。

testing pyramid

根据测试金字塔理论,客户端自动化测试因为以下几个问题,导致其性价比不如集成测试。

  1. UI 层变化太大,测试用例需要频繁改动,维护成本较高;
  2. 集成度最高,难以准确定位故障;
  3. 用真实设备来实施测试,实施成本最高;
  4. 客户端的进程过多,导致其测试以外的干扰因素过多,因此稳定性较差;

因为业务独特的情况,这几个问题均可得到一定程度的削弱,可行性反而较高:

  1. 游戏 SDK 有对应的客户端 DEMO,UI 层变化很小,即使变化往往也是 UI 树增加子节点,不影响定位原来的元素。
  2. 由于游戏 SDK 的核心功能只有登录和支付,业务链路单一且功能相对平行,不会有太多的业务耦合,故障定位比较简单。举个栗子:支付宝支付挂了,要么是支付宝支付链路上的问题,不会影响微信支付的用例,除非是支付底层逻辑问题。退一步讲,上述情况发生了,定位也是比较容易的。
  3. 无论是 Android Studio 还是 Xcode,其实都是支持模拟器的,我们可以借助模拟器来做测试,可以极大地降低硬件成本。可行原因参考本文:2.关于使用模拟器做自动化测试的调研。
  4. 由于创建的模拟器没有安装太多应用,应用之间的干扰比较小。且模拟器在电脑中,网络环境比较好。

1.3 常见误区

1.3.1 期望客户端自动化测试能够测试出兼容性问题

客户端自动化测试不等于兼容性测试,不需要测试各种机型,各种屏幕的设备。那是兼容性测试的工作,自动化测试的核心还是降低测试执行成本,确认质量符合要求。

1.3.2 期望测试用例稳定性到 100%,不能存在误报

任何测试都会存在干扰因素,不可能 100% 稳定:硬件设备断电,网络不稳定,进程卡死,服务端测试环境故障,服务端和客户端配置错误,测试框架出 Bug,甚至真机设备上的系统弹窗都会导致测试用例失败。误报确实是衡量客户端测试有效性的重要因素,但不应该成为衡量的唯一因素。

我们应该秉持的态度是:一方面在误报出现时,尽快由测试人员介入确认该误报;另一方面不断完善测试框架,尽可能消除这些干扰因素。

1.4 技术栈选型

团队成员对 Python 和 Airtest 比较熟悉,并且之前有成熟的测试项目。Pytest 有丰富的测试插件,开发效率较高。此外可以自己编写钩子,本测试框架在配置测试环境、生成测试报告、发送测试邮件等环节多次用到了钩子。为了快速出成果,故选用了 Pytest + Airtest。

此外,为了紧跟前沿测试,使用了行为驱动开发的思想。测试驱动开发的思想,想必大家都有所耳闻。行为驱动开发,其实是前者的一种升级。应用在测试领域,就是行为驱动测试,它定义了每个测试步骤,并赋予了这些测试步骤一定的代码,使其能够实现这个测试步骤。如果只做到这些,那还不够诱人,行为驱动测试允许测试步骤的复用。具体示例请参考本文:4.1 测试脚本

Behavior-driven development is an extension of test-driven development, a development process that makes use of a simple DSL. These DSLs convert structured natural language statements into executable tests. The result is a closer relationship to acceptance criteria for a given function and the tests used to validate that functionality. As such it is a natural extension of TDD testing in general.

From Wikipedia: Behavior-driven development

2.关于使用模拟器做自动化测试的调研

正如前文所说,可以创建客户端模拟器用于客户端自动化测试,可以降低硬件成本。对于这一设想的可行性,作者花了较多时间做了深度调研,打通了模拟器客户端自动化测试的各个环节。

2.1 创建模拟器

对于安卓设备有:

# 切换到Android sdk的根目录
cd /Users/<username>/Library/Android/sdk

# 创建模拟器
./tools/bin/avdmanager create avd -n <emulator_name> -k "system-images;android-22;google_apis;arm64-v8a"

# 重命名模拟器
./tools/bin/avdmanager move avd -n <emulator_name> <new_emulator_name>

# 删除模拟器
./tools/bin/avdmanager delete avd -n <emulator_name>

# 启动模拟器
./emulator/emulator -avd Pixel_5_API_22 & bg

# 关闭模拟器
kill `ps aux | grep Android/sdk/emulator/qemu | xargs | awk -F ' ' 'END{print $2}'`

对于 iOS 设备:

# 创建模拟器,机型iPhone-13-Pro-Max,系统版本iOS-15-2
xcrun simctl create my_iphone_pro_max com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro-Max com.apple.CoreSimulator.SimRuntime.iOS-15-2

# 删除模拟器
xcrun simctl delete my_iphone_pro_max

# 启动模拟器
xcrun simctl boot my_iphone_pro_max

# 关闭模拟器
xcrun simctl shutdown my_iphone_pro_max

# 检查设备的运行状态
xcrun simctl bootstatus my_iphone_pro_max

# 找到正在启动的模拟器设备
xcrun simctl list | grep Booted

# 给设备起一个别名
xcrun simctl 7CD4F140-0BDC-4AE9-8FA0-960D33055044 rename my_iphone_pro_max

simctl提供了更为丰富的 api,可以实现更多的功能。

使用 Python 代码封装上述的命令,即是整个框架的底层工具包。

2.2 连接模拟器

连接模拟器的教程,网易的 Airtest 文档朱玉在前,我就不做赘述,请参考:

如何在 iOS 手机上进行自动化测试

如何在 Android 手机上进行自动化测试(上)

如何在 Android 手机上进行自动化测试(下)

# Tiffany.py

from poco.drivers.ios import iosPoco
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from airtest.core.api import auto_setup, connect_device

from cases.utils import config_parser


class Tiffany(object):
    """
    底层工具箱
    """

    def __init__(self, app_name: str) -> None:
        self.app_name = app_name

    def make_android_poco(self) -> None:
        auto_setup(__file__)
        connect_device(uri=config_parser.get("DEVICE", "EMULATOR"))
        self.poco = AndroidUiautomationPoco(
            pre_action_wait_for_appearance=3,
            action_interval=0.5,
            poll_interval=0.4
        )

    def make_ios_poco(self) -> None:
        auto_setup(__file__, devices=[config_parser.get("DEVICE", "IPHONE_URI"), ]
        connect_device(config_parser.get("DEVICE", "IPHONE_URI"))
        self.poco = iosPoco(
            pre_action_wait_for_appearance=4,
            action_interval=0.1,
            poll_interval=0.2
        )

2.3 操作模拟器

对于 Android 设备,有祖传的 adb 命令来操作:

# 列出连接的设备
./platform-tools/adb devices

# 列出app
./platform-tools/adb -s Pixel_5_API_22 shell pm list packages

# 安装测试应用
./platform-tools/adb -s Pixel_5_API_22 install <path_to_apk>.apk

# 启动pocoservice
./platform-tools/adb shell am start -n com.netease.open.pocoservice/com.netease.open.pocoservice.TestActivity

# 启动测试应用
./platform-tools/adb -s Pixel_5_API_22 shell am start com.example.sdk.demo.activity.SplashActivity

# 卸载测试应用
./platform-tools/adb -s Pixel_5_API_22 uninstall com.example.sdk.demo

对于 iOS 设备,有xcrun simctl命令来操作:

# 安装应用到指定的设备
xcrun simctl install my_iphone_pro_max resources/packages/IOSDemoMainland.app

# 卸载指定应用,根据bundle_id
xcrun simctl uninstall my_iphone_pro_max com.netease.boltrend.sdk.demo

# 列出已安装的应用
xcrun simctl listapps my_iphone_pro_max

# 查看app信息
xcrun simctl appinfo my_iphone_pro_max com.apple.mobilesafari

# 启动app
xcrun simctl launch my_iphone_pro_max com.apple.mobilesafari

# 关闭app
xcrun simctl terminate my_iphone_pro_max com.apple.mobilesafari

# 打开url
xcrun simctl openurl my_iphone_pro_max https://cn.bing.com/

# Sure, you can use the global macOS shortcut to capture a screenshot of a simulator (⇧⌘4), but if you intend to use those for your App Store listing, you’re much better off using the io subcommand:
$ xcrun simctl io booted screenshot app-screenshot.png

# You can also use this subcommand to capture a video as you interact with your app from the simulator (or don’t, in the case of automated UI tests):
$ xcrun simctl io booted recordVideo app-preview.mp4

此外,tidevice 也是好用的工具。详细用法请参考:阿里 Python 自动化工具 tidevice 使用指南

3.总体设计

总体设计架构如下:

客户端自动化测试

4.详细设计

测试框架设计

4.1 测试脚本

下面重点讲讲测试脚本编写:

4.1.1 定义测试步骤

针对一个邮箱登录的测试场景,可以用 gherkin 语言定义下面的测试步骤:

# mail_login_test.feature
Feature: mail_login_test
  testcases about mail login

  Scenario: mail login with correct password
    Given start app and initialize settings
    And open the main board
    When I admit agreements on login board
    And I go to the mail login board
    And I input correct password and login
    Then assert login success

4.1.2 编写测试代码

每个测试步骤,都有对应的

# mail_login_test.py

from pytest_bdd import given, then, when


@given('start app and initialize settings')
def start_app_and_initialize_settings() -> None:
    """
    启动app,初始化设置
    """
    pass


@given('open the main board')
def open_the_main_board() -> None:
    """
    打开登陆面板
    """
    pass


@when("I admit agreements on login board")
def admit_agreements_on_login_board() -> None:
    """
    同意用户协议
    """
    pass


@when("I go to the mail login board")
def to_mail_login_board() -> None:
    """
    到邮箱账号登录面板
    """
    pass


@when("I input correct password and login")
def input_correct_password_and_mail_login() -> None:
    """
    邮箱登录,输入正确的密码
    """
    pass


@then('assert login success')
def login_success() -> None:
    """
    断言登陆成功
    """
    pass


@scenario('mail_login.feature', 'mail login with correct password')
def test_mail_login_with_correct_password() -> None:
    """
    邮箱登录测试用例
    """
    pass

4.1.3 代码复用示例

可复用的步骤可以放到conftest.py中,比如:start app and initialize settings,open the main board,I admit agreements on login board,代码示例如下:

# conftest.py

def initialize_environment(apk_name: str) -> None:
    """
    初始化测试环境
    :param apk_name:  apk包名
    :return: None
    """
        pass

对于支付宝支付的测试用例,需要登陆操作,显然可以复用邮箱登录的步骤。

# alipay_test.feature

Feature: alipay_test
  testcases about alipay

  Scenario: 支付宝支付,编辑商品信息,拉起支付窗口,验证支付是否调起
    Given start app and initialize settings
    And open the main board
    When I admit agreements on login board
    And I go to the mail login board
    And I input correct password and login
    Then assert login success
    When I prepare the order information with CNY
    And I place an order of alipay
    Then alipay cashier desk will show up

由于复用了邮箱登录的步骤,上述拉起支付宝窗口的用例,只需要专注于下订单拉起支付的逻辑。

# alipay_test.py

@when("I prepare the order information with CNY")
def prepare_order_information() -> None:
    """
    准备订单信息
    """
    pass

@when("I place an order of alipay")
def place_an_order() -> None:
    """
    下订单
    """
    pass

@then("alipay cashier desk will show up")
def assert_alipay_cashier_desk_shows_up() -> None:
    """
    展示收银台
    """
    pass

4.1.4 测试数据统计

Pytest 有钩子支持二次开发,在pytest_runtest_makereport钩子内。

def pytest_runtest_makereport(item, call) -> None:
    """
    统计测试数据
    """
    pass

4.1.5 回传统计数据

在钩子pytest_runtest_makereport(item, call)编写数据持久化逻辑。若使用 jenkins,则直接往数据库写数据。若使用自研服务,则调用服务端接口,回传测试数据。

def pytest_sessionfinish(session, exitstatus) -> None:
    """
    调用调度服务的接口,上传测试数据
    """
    pass

4.1.6 发送测试报告

jinja2库支持编写邮件网页,email库支持发送邮件。

4.2 调度服务

4.2.1 调度服务设计

调度服务和 Web 前端,可以用 jenkins 代替。相应地,开发成本降低,可以快速出效果,但是展示效果要打折扣,数据处理逻辑也更依赖 pytest 钩子的二次开发。

服务端框架推荐:Flask,可以快速出成果。以创建任务为例:

@app.route("/testtask/create", methods=["POST"])
def create_test_task() -> str:
    pass

4.2.2 远程执行命令

本地机器生成私钥和公钥

$ ssh-keygen -t rsa

把公钥存放到远程机器上

$ scp ~/.ssh/id_rsa.pub zhancc@192.168.94.111:~/.ssh/id_rsa.pub

把公钥添加到远程机器的authorized_keys

cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

远程执行命令

ssh zhancc@192.168.94.111 "/Users/<username>/Library/Android/sdk/tools/bin/avdmanager create avd -n <emulator_name> -k system-images;android-22;google_apis;arm64-v8a"

Python 调用命令

import subprocess


def execute_bash(command: str, timeout: int = 10) -> tuple:
    """
    执行shell脚本
    :param command: 脚本
    :param timeout: 超时时间,默认为10秒
    :return: stdout, stderr 标准输出和错误
    """
    sp = subprocess.Popen(command, shell=True)
    return sp.communicate(timeout=timeout)

4.2.3 支持异步任务

各种命令调用,用同步任务,性能将会很差,且模块耦合较高。使用 celery 异步任务将会提高性能,以调用远程命令为例:

from utils import Machine
from celery import Celery

celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)


@celery.task
def execute_remote_command(machine: Machine, command):
    """
    执行远程命令
    """
    pass

4.2.4 支持定时任务

为了支持定时触发测试任务,推荐使用APScheduler

from apscheduler.schedulers.background import BlockingScheduler

scheduler = BlockingScheduler()
scheduler.add_executor('processpool')


def trigger_test() -> None:
    """
    触发测试任务
    """
    pass


scheduler.add_job(
    ui_test, args=("iOS_SDK_Client_Test", "mainland", "qa"),
    trigger='cron', minute="07", hour="5", timezone=ZoneInfo("Asia/Shanghai")
)

scheduler.add_listener(task_listener, EVENT_JOB_ERROR)

print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))

try:
    scheduler.start()
except (KeyboardInterrupt, SystemExit):
    pass

4.2.5 Web 后台

利用 Vue.js 快速实现 Web 后台,在此不过多介绍。

5.结语

5.1 应用拓展

5.2 作者的话

今年 9 月份找到公司法务,想把这个发成客户端自动化测试专利,法务说你这个方案与一些已有专利相似度较高。既然发不了,不如就分享给大家,共勉!

6.参考文档

  1. The Testing Pyramid: Simplified for One and All
  2. Just Say No to More End-to-End Tests
  3. Android Studio - avdmanager
  4. simctl Written by Mattt November 26th, 2018
  5. Best iOS simulators for Windows and Mac for 2022
  6. 阿里 Python 自动化工具 tidevice 使用指南
  7. 如何在 Android 手机上进行自动化测试(上)
  8. 如何在 Android 手机上进行自动化测试(下)
  9. 如何在 iOS 手机上进行自动化测试
  10. BDD (Behavior Driven Development) Framework: A Complete Tutorial
  11. Behavior-Driven Development(BDD) Testing
  12. ssh 密钥登录及远程执行命令
  13. Pytest Documentation
  14. Flask Documentation
  15. APScheduler Documentation
  16. Celery - 分布式任务队列


↙↙↙阅读原文可查看相关链接,并与作者交流