实施背景:

销帮帮自2015年成立以来,移动端UI自动化测试的落地举步维艰,移动端的UI自动化测试一直以来都未取得良好的落地。

然而移动互联网时代,怎样落地移动端的UI自动化测试以快速稳定进行移动端的端到端回归测试始终是一个优秀的质量保障方案。

方案探索

1.1 Appiums

  • 劣势:配置复杂,用例执行速度慢,不稳定,自动化用例脚本维护成本高。

1.2 AirTest

  • 劣势:基于图形比对,误报率高,脚本可维护性差。

1.3 UiAutomator2

  • 劣势:配置复杂,用例执行速度慢,不稳定。

Playwright的落地

2024年WebUI自动化框架从Selenium迁移到Playwright并稳定运行3个季度,从稳定性和执行速度方面来看,Playwright都证明它远远比Selenium更优秀。

基于Web端的UI自动化测试实践效果考虑,移动端UI自动化技术也使用Playwright。

2.1、移动端UI自动化测试的难点

销帮帮CRM的移动端产品形态多样化严重,其分为钉钉,企业微信,飞书以及独立版APP。

若想使用一套技术框架实现多种产品形态的UI自动化测试,必须要求平台无关性和用户登录态信息的模拟以及移动端设备的tap事件模拟!

2.2、Playwright模拟移动端设备

class Base:    def __init__(self):        self.app_host_str = "https://dingtalkapi.xbongbong.com/pro"        self.playwright = sync_playwright().start()        iphone_15 = self.playwright.devices['iPhone 15 Pro Max']        self.browser = self.playwright.chromium.launch(headless=False)        self.context = self.browser.new_context(            **iphone_15,        )

Playwright默认支持一批移动端设备的模拟,虽然其可以模拟的设备有限,但足够用了。Playwright官网详细列举了它支持的移动端设备列表。

  • 可以到我的个人号:atstudy-js,看完整版内容

  • 这里有10W+ 热情踊跃的测试小伙伴们,一起交流行业热点、测试技术各种干货,一起共享面试经验、跳槽求职各种好用的。        

  • 多行业测试学习交流群,内含直播课+实战+面试资料

    AI测试、 车载测试、自动化测试、银行、金融、游戏、AIGC.

2.3、绕过平台无关性

开发同学给出的JS原生方法:​​​​​​​

// 在任何脚本执行前注入正则修改代码// 用于开启移动端的本地开发模式await chrome.debugger.sendCommand(  { tabId: tab.id },  "Page.addScriptToEvaluateOnNewDocument",  {    source: `            const originalTest = RegExp.prototype.test;            RegExp.prototype.test = function() {                const pattern = this.source;                if (pattern === '^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+$') {                    return true;                }                return originalTest.apply(this, arguments);            };          `,  });

将上述JavaScript代码改写为Python代码:​​​​​​​

class Base:    def __init__(self):        self.app_host_str = "https://dingtalkapi.xbongbong.com/pro"        self.playwright = sync_playwright().start()        iphone_15 = self.playwright.devices['iPhone 15 Pro Max']        self.browser = self.playwright.chromium.launch(headless=False)        self.context = self.browser.new_context(            **iphone_15,        )        self.context.add_init_script("""                    const originalTest = RegExp.prototype.test;                    RegExp.prototype.test = function() {                        const pattern = this.source;                        if (pattern === '^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+$') {                            return true;                        }                        return originalTest.apply(this, arguments);                    };                """)

2.4、LocalStorage模拟用户登录态

开发同学给出的JS原生方法:​​​​​​​

const { chromium } = require('playwright');(async () => {  const browser = await chromium.launch({ headless: false });  const context = await browser.newContext();  // 方法1:初始化时注入  await context.addInitScript(() => {    localStorage.setItem('userId', '');    localStorage.setItem('corpid', '');    localStorage.setItem('xbbAccessToken', '');  });  const page = await context.newPage();  await page.goto('https://example.com');  await browser.close();})();

将上述JavaScript代码改写为Python代码:

self.context.add_init_script("""                localStorage.setItem('userId', 'manager9426');                localStorage.setItem('corpid', 'ding18b527cbc48f835535c2f4657eb6378f');                localStorage.setItem('xbbAccessToken', 'f8c7346d2e7eb000088740324734bda223abb893e1e882a2851e1eafcd86e565');            """)

2.5、封装通用方法

2.5.1 输入编号信息(系统中的编号-流水号字段)​​​​​​​

    def input_serial(self, send_keys):        self.page.get_by_placeholder("流水号", exact=True).clear()        self.page.get_by_placeholder("流水号", exact=True).fill(send_keys)

2.5.2 locator操作控件并输入内容 支持css-selector xpath格式​​​​​​​

    def input_by_locator(self, input_index, input_context):        input_ele_list = self.page.locator('//input[@placeholder="请输入"]').locator("visible=true")        input_ele_list.nth(input_index).clear()        input_ele_list.nth(input_index).fill(input_context)

2.5.3 点击button组件​​​​​​​

    def click_button(self, button_name):        self.sleep(1000)        self.page.get_by_role("button", name=button_name, exact=True).click()        if str(button_name) in "提交确定修改删除":            self.sleep(2000)        else:            self.sleep(1000)

2.5.4 列表页搜索​​​​​​​

    def search_on_list(self, search_condition):        self.page.locator('//span[@class="search__text"]').click()        self.page.locator('//input[@type="search"]').fill(search_condition)        self.page.keyboard.down('Enter')        self.sleep(500)        self.page.keyboard.up('Enter')        self.sleep(1000)

2.5.5 点击列表页右上角的"新建"​​​​​​​

    def new_on_list(self):        self.page.locator('//span[@class="right-menu"]').locator("visible=true").click()        self.sleep(1000)

2.5.6 跳转到指定列表/单据详情​​​​​​​

    def go(self, url_key):        list_url = self.url_dict[url_key]        if str(url_key).__contains__("index"):            self.page.goto(self.app_host_str + list_url)        else:            self.page.goto(self.app_host_str + str(self.url_dict["index_page"]) + str(list_url))        self.sleep(3000)

2.5.7 判断系统里一闪而过的tips文案​​​​​​​

   def assert_action_success(self):        action_tips_list = self.page.locator('//p[@class="el-message__content"]').locator("visible=true")        expect(action_tips_list.nth(0)).to_contain_text("成功")

2.5.8 判断列表搜索结果不为空​​​​​​​

    def assert_list_search_result(self):        expect(self.page.locator('//div[@class="card-content"]')).to_be_visible()        expect(self.page.locator('//div[@class="no-data-hint"]')).to_be_hidden()

2.5.9 模拟移动端的tap事件​​​​​​​

    def tap_single_element(self, locator_str):        # 模拟单指触摸        self.page.locator(locator_str).locator("visible=true").tap()        self.sleep(1000)

2.5.10 模拟tap事件 触发关联数据选择浮层​​​​​​​

        element_list = self.page.locator('//div[@class="xb-picker-cell"]').locator('//span[@class="xb-picker-cell__placeholder"]').filter(has_text="请选择").locator("visible=true")        # 模拟单指触摸        element_list.nth(element_index).tap()        self.sleep(1000)

2.5.11 关联数据选择 支持系统和自定义

​​​​​​​​​

    def select_relation_data(self, element_index, relation_data_name):        # 模拟移动端tap事件        self.touch_screen(element_index)        # 点击搜索        self.page.locator('//div[@class="selection_header"]/div[2]/span').locator("visible=true").click()        # 输入关联数据的搜索条件        self.page.locator('//form[@target="frameFile"]/div/span[1]/div/input').locator("visible=true").fill(relation_data_name)        self.page.keyboard.down('Enter')        self.sleep(500)        self.page.keyboard.up('Enter')        self.sleep(500)        # 模拟移动端tab 选中搜索出的关联数据        self.tap_single_element('//div[@class="selection_item_new"]')        # 确认选中关联数据        self.page.locator('//span[@class="selection_btn__text"]').filter(has_text="确认").locator("visible=true").click()        self.sleep(3000)

2.5.12 单据详情页右上角-更多操作​​​​​​​

def action_on_detail(self, action_name):    self.page.locator('//span[@class="right-menu"]').filter(has_text="操作").locator("visible=true").click()    self.sleep(1000)    # 当前页面查找li元素    li_ele_list = self.page.locator('//li[@class="xb-select-li click-active"]').locator("visible=true")    for element in li_ele_list.all():        action_ele = element.locator('//span')        if action_ele.inner_text().__contains__(action_name):            element.tap()            break        else:            continue    self.sleep(1000)

2.5.13 单据新建/编辑页 选择所有关联产品

    def select_products(self, element_index):        self.touch_screen(element_index)        self.sleep(1000)        self.tap_single_element('//div[@class="pro-right"]')        self.click_button("确定")        self.click_button("保存")

2.5.14 列表页 删除跟进记录​​​​​​​

    def del_communicate(self):        self.tap_single_element('//i[@class="app-icon-shanchu app-iconfont operation"]'

更多推荐