Skip to content

【线上】用户端App自动化测试

用户端 App 自动化测试

预习准备

  • 提前先预习完以下相关的知识,再开始本章节的学习。
知识模块 等级 知识点 Python 班级 Java 班级
用户端 APP 自动化测试 L1 全部知识点
appium v2 版本教程
appium v1 版本录播
appium v2 版本录播
Java 版录播
用户端 APP 自动化测试 L2 capability 进阶用法
用户端 APP 自动化测试 L2 元素定位工具
用户端 APP 自动化测试 L2 高级定位技巧-xpath 定位
用户端 APP 自动化测试 L2 特殊控件 toast 识别
用户端 APP 自动化测试 L2 显式等待高级使用
用户端 APP 自动化测试 L2 高级控件交互方法

课程目标

  • 了解 Appium 原理
  • 熟悉 Appium 框架与常用操作
  • 掌握 App 自动化测试用例编写能力
  • 掌握 App 自动化测试实战能力

App 自动化测试知识体系

点击查看:App 自动化测试知识点梳理.xmind

Appium 原理解析

点击查看:Appium 原理解析

实战练习

需求说明

被测对象
  • 企业微信
  • 腾讯微信团队打造的企业通讯与办公工具,具有与微信一致的沟通体验,丰富的 OA 应用,和连接微信生态的能力。
  • 可帮助企业连接内部、连接生态伙伴、连接消费者。专业协作、安全管理、人即服务。
  • 前提条件:
  • 手机端安装好企业微信 App。
  • 企业微信注册用户。
测试需求
  • 企业微信 App 搜索联系人功能自动化。
  • 企业微信 App 添加成员功能自动化测试。

实战思路

uml diagram

实战 1:搜索联系人测试场景

测试模块 用例标题 前置条件 用例步骤 预期结果 实际结果
成员模块 查询成员 登录成功 1. 进入通讯录页面
2. 点击搜索按钮,输入一个已经存在的成员名称。
1. 搜索结果列表页面展示该成员信息,包含其成员名称,以及公司名称。
  1. 在实现过程中,需要将代码根据测试步骤实现出来。
  2. 如果碰到某些控件不好定位,则需要灵活运用 xpath 的语法表达式对控件进行定位:高级定位技巧-xpath 定位
Python 实现
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.options.common import AppiumOptions


class TestWework:

    def setup(self):
        # Capability 设置定义为字典
        caps = {}
        # 设置 app 安装的平台(Android、iOS)
        caps["platformName"] = "Android"
        # 设置 app 安装平台的版本
        caps["appium:platformVersion"] = "6"
        # 设备的名字
        caps["appium:deviceName"] = "MuMu"
        # 设置 app 的包名
        caps["appium:appPackage"] = "com.tencent.wework"
        # 设置 app 启动页
        caps["appium:appActivity"] = ".launch.LaunchSplashActivity"
        # 不清空缓存
        caps["appium:noReset"] = True
        # app 不重启
        # caps["appium:dontStopAppOnReset"] = True
        options = AppiumOptions().load_capabilities(caps)
        # 初始化 driver
        self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", options=options)
        # 设置全局的隐式等待
        self.driver.implicitly_wait(10)

    def teardown(self):
        # 关闭 driver
        self.driver.quit()

    def test_search_contact(self):
        '''
        冒烟用例,搜索存在的联系人
        '''
        search_key = "蚊子"
        # 点击通讯录按钮
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='通讯录']").click()
        # 点击搜索按钮
        self.driver.find_element(
            # ID 定位,id在不同手机上可能会变化,所以不推荐使用
            # AppiumBy.ID,
            # "com.tencent.wework:id/lyp").click()
            AppiumBy.XPATH,
            # xpath 轴定位
            '//*[@text="悠然科技"]/../../../following-sibling::*/*[1]').click()
            # xpath 父子定位
            # '//*[@text="悠然科技"]/../../../../*[3]/*[1]').click()
        # 查找搜索框,输入搜索关键词
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='搜索']").send_keys(search_key)
        # 获取搜索结果元素的文本,完成断言
        eles = self.driver.find_elements(
            AppiumBy.XPATH,
            '//*[@class="android.widget.ListView"]//*[@class="android.widget.ImageView"]/../../following-sibling::*/*[1]/*[1]')
        assert search_key in [ele.text for ele in eles]
Java 实现
import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebElement;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class TestMember {

    public static AndroidDriver driver;
    @BeforeAll
    public static void setUpClass() {
        //配置信息
        UiAutomator2Options uiAutomator2Options = new UiAutomator2Options()
                .setPlatformName("Android")
                .setAutomationName("uiautomator2")
                .setNoReset(true)
                .amend("appium:appPackage", "com.tencent.wework")
                .amend("appium:appActivity", ".launch.LaunchSplashActivity")
                .amend("appium:forceAppLaunch", true)
                .amend("appium:shouldTerminateApp", true);
        //初始化
        try {
            driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), uiAutomator2Options);
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
    @AfterAll
    public static void tearDownClass(){
        driver.quit();
    }
    /**
     * 前置条件: 登录成功
     * 1. 进入通讯录页面
     * 2. 点击搜索按钮,输入一个已经存在的成员名称。
     */
    @Test
    public void searchMember() {

        driver.findElement(AppiumBy.xpath("//*[@text='通讯录']")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='Hufflepuff']/../../../../*[3]/*[1]")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='搜索']")).sendKeys("张三");
        List<WebElement> webElements =  driver.findElements(AppiumBy.xpath("//*[@class='androidx.recyclerview.widget.RecyclerView']//*[@class='android.widget.LinearLayout']//*[@class='android.widget.TextView']"));
        //  1. 遍历所有元素 2. 获取所有元素对应的文本 3. 判断是否包含张三 4. 返回判断结果,如果所有元素都包含,则返回true
        assertTrue(webElements.stream().allMatch(item -> item.getText().contains("张三")));
    }

}

实战 2:添加联系人测试场景

测试模块 用例标题 前置条件 用例步骤 预期结果 实际结果
成员模块 添加成员-成功 登录成功 1. 进入通讯录页面
2.点击添加成员,手动输入添加
3.输入正确的成员信息,点击保存
1. 成功添加学员,并给出“添加成功”的提示信息。
2. 添加成功的成员展示在成员列表里面
成员模块 添加成员-手机号重复,添加失败 登录成功 1. 进入通讯录页面
2.点击添加成员,手动输入添加
3.输入已经存在的手机号,点击保存
1. 提示“手机已存在于通讯录,无法添加”
2. 添加成员失败。
  1. 如果需要获取 toast 的文本信息做验证,可以参考:特殊控件 toast 识别
  2. 如果需要将代码滑动处理,则需要封装一个滑动操作的的方法。
Python 实现
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.options.common import AppiumOptions
from faker import Faker


class TestWework:

    def setup_class(self):
        faker = Faker("zh_CN")
        self.name = faker.name()
        self.phonenum = faker.phone_number()

    def setup(self):
        # Capability 设置定义为字典
        caps = {}
        # 设置 app 安装的平台(Android、iOS)
        caps["platformName"] = "Android"
        # 设置 app 安装平台的版本
        caps["appium:platformVersion"] = "6"
        # 设备的名字
        caps["appium:deviceName"] = "MuMu"
        # 设置 app 的包名
        caps["appium:appPackage"] = "com.tencent.wework"
        # 设置 app 启动页
        caps["appium:appActivity"] = ".launch.LaunchSplashActivity"
        # 不清空缓存
        caps["appium:noReset"] = True
        # 实例化 AppiumOptions 对象
        options = AppiumOptions().load_capabilities(caps)
        # 初始化 driver
        self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", options=options)
        # 设置全局的隐式等待
        self.driver.implicitly_wait(10)

    def teardown(self):
        # 关闭 driver
        self.driver.quit()

    def test_add_contact(self):
        '''
        添加联系人
        1。 点击通讯录,进入通讯录页面
        2。 点击添加成员按钮,进入添加成员页面
        3。 点击手动输入添加按钮,输入成员信息页面
        5。 输入姓名,手机号,点击保存按钮
        6。 返回添加成员页面
        '''
        # 点击通讯录按钮
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='通讯录']").click()
        # 滑动点击添加成员按钮
        self.swipe_find("添加成员").click()
        # 点击手动输入添加按钮
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='手动输入添加']").click()
        # 定位姓名输入框
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[contains(@text, '姓名')]/../*[@text='必填']").send_keys(self.name)
        # 定位手机号输入框
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[contains(@text, '手机')]/..//*[@text='必填']").send_keys(self.phonenum)
        # 点击保存按钮
        self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='保存']").click()
        tips = self.driver.find_element(
            AppiumBy.XPATH,
            "//*[@class='android.widget.Toast']").text
        assert tips == "添加成功"

    def swipe_window(self):
        '''
        滑动界面
        '''
        # 滑动操作
        # 获取设备的尺寸
        size = self.driver.get_window_size()
        # {"width": xx, "height": xx}
        print(f"设备尺寸为 {size}")
        width = size.get("width")
        height = size.get('height')
        # # 获取滑动操作的坐标值
        start_x = width / 2
        start_y = height * 0.8
        end_x = start_x
        end_y = height * 0.2
        # swipe(起始x坐标,起始y坐标,结束x坐标,结束y坐标,滑动时间(单位毫秒))
        self.driver.swipe(start_x, start_y, end_x, end_y, 2000)

    def swipe_find(self, text, max_num=5):
        '''
        滑动查找
        通过文本来查找元素,如果没有找到元素,就滑动,
        如果找到了,就返回元素
        '''
        # 为了滑动操作更快速,不用等待隐式等待设置的时间
        self.driver.implicitly_wait(1)
        for num in range(max_num):
            try:
                # 正常通过文本查找元素
                ele = self.driver.find_element(AppiumBy.XPATH, f"//*[@text='{text}']")
                print("找到元素")
                # 能找到则把隐式等待恢复原来的时间
                self.driver.implicitly_wait(15)
                # 返回找到的元素对象
                return ele
            except Exception:
                # 当查找元素发生异常时
                print(f"没有找到元素,开始滑动")
                print(f"滑动第{num + 1}次")
                # 滑动操作
                self.swipe_window()
        # 把隐式等待恢复原来的时间
        self.driver.implicitly_wait(15)
        # 抛出找不到元素的异常
        raise NoSuchElementException(f"滑动之后,未找到 {text} 元素")
Java 实现

public class TestMember {

    public static AndroidDriver driver;
    @BeforeAll
    public static void setUpClass() {
        //配置信息
        UiAutomator2Options uiAutomator2Options = new UiAutomator2Options()
                .setPlatformName("Android")
                .setAutomationName("uiautomator2")
                .setNoReset(true)
                .amend("appium:appPackage", "com.tencent.wework")
                .amend("appium:appActivity", ".launch.LaunchSplashActivity")
                .amend("appium:forceAppLaunch", true)
                .amend("appium:shouldTerminateApp", true);
        //初始化
        try {
            driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), uiAutomator2Options);
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
    @AfterAll
    public static void tearDownClass(){
        driver.quit();
    }



    @ParameterizedTest
    @CsvSource({"李四, 13188890000"})
    public void addMember(String name, String phone) throws InterruptedException {
        driver.findElement(AppiumBy.xpath("//*[@text='通讯录']")).click();
        for(int i =1; i<10; i++) {
            try {
                driver.findElement(AppiumBy.xpath("//*[@text='添加成员']")).click();
                break;
            }catch (Exception e) {
                scrollDown();
            }
        }
        driver.findElement(AppiumBy.xpath("//*[@text='手动输入添加']")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='姓名']//../*[@class='android.widget.EditText']")).sendKeys(name);
        driver.findElement(AppiumBy.xpath("//*[@text='手机']/..//*[@class='android.widget.EditText']")).sendKeys(phone);
        driver.findElement(AppiumBy.xpath("//*[@text='保存']")).click();
        String toastMessage = driver.findElement(AppiumBy.xpath("//*[@class='android.widget.Toast']")).getText();
        driver.navigate().back();
        driver.findElement(AppiumBy.xpath("//*[@text='Hufflepuff']/../../../../*[3]/*[1]")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='搜索']")).sendKeys(name);
        List<WebElement> webElements =  driver.findElements(AppiumBy.xpath("//*[@class='androidx.recyclerview.widget.RecyclerView']//*[@class='android.widget.LinearLayout']//*[@class='android.widget.TextView']"));
        //  1. 遍历所有元素 2. 获取所有元素对应的文本 3. 判断是否包含张三 4. 返回判断结果,如果所有元素都包含,则返回true
        assertTrue(webElements.stream().allMatch(item -> item.getText().contains("李四")));
        assertEquals("添加成功", toastMessage);
    }

    public static void scrollDown() {
        // 获取屏幕高度和宽度
        int height = driver.manage().window().getSize().getHeight();
        int width = driver.manage().window().getSize().getWidth();
        // 设置滑动起始和结束坐标
        int startY = (int) (height * 0.8);
        int startX = width / 2;
        int endY = (int) (height * 0.2);
        TouchAction touchAction = new TouchAction(driver);
        touchAction.longPress(ElementOption.point(startX, startY))
                .moveTo(ElementOption.point(startX, endY))
                .release()
                .perform();
    }

}

实战 3:用例优化

  1. 用例执行过程中添加截图。
  2. 将截图保存到 allure 报告中。
Python 实现
import os
import time

import allure
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.options.common import AppiumOptions

root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


@allure.feature("企业微信联系人操作")
class TestWithShot:

    def setup(self):
        # Capability 设置定义为字典
        caps = {}
        # 设置 app 安装的平台(Android、iOS)
        caps["platformName"] = "Android"
        # 设置 app 安装平台的版本
        caps["appium:platformVersion"] = "6"
        # 设备的名字
        caps["appium:deviceName"] = "MuMu"
        # 设置 app 的包名
        caps["appium:appPackage"] = "com.tencent.wework"
        # 设置 app 启动页
        caps["appium:appActivity"] = ".launch.LaunchSplashActivity"
        # 不清空缓存
        caps["appium:noReset"] = True
        # app 不重启
        # caps["appium:dontStopAppOnReset"] = True
        # 实例化 AppiumOptions 对象
        options = AppiumOptions().load_capabilities(caps)
        # 初始化 driver
        self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", options=options)
        # 设置全局的隐式等待
        self.driver.implicitly_wait(10)

    def screenshot(self):
        '''
        截图
        :param path: 截图保存路径
        '''
        cur_time = time.strftime("%Y-%m-%d-%H-%M-%S")
        file_path = cur_time + ".png"
        dir_path = os.sep.join([root_path, "screenshot"])
        # 资源目录如果不存在则新创建一个
        if not os.path.isdir(dir_path):
            os.mkdir(dir_path)
        # 截图保存路径
        source_path = os.sep.join([dir_path, file_path])
        # 截图
        self.driver.save_screenshot(source_path)
        # 返回保存图片的路径
        return source_path

    @allure.story("搜索联系人")
    def test_search_contact(self):
        '''
        冒烟用例,搜索存在的联系人
        '''
        search_key = "蚊子"
        # 点击通讯录按钮
        with allure.step("点击通讯录按钮"):
            image_path = self.screenshot()
            self.driver.find_element(
                AppiumBy.XPATH,
                "//*[@text='通讯录']").click()
            allure.attach.file(image_path, name="点击通讯录按钮", attachment_type=allure.attachment_type.PNG)
        # 点击搜索按钮
        with allure.step("点击搜索按钮"):
            image_path = self.screenshot()
            self.driver.find_element(
                AppiumBy.XPATH,
                '//*[@text="悠然科技"]/../../../'
                'following-sibling::*/*[1]').click()
            allure.attach.file(image_path, name="点击搜索按钮", attachment_type=allure.attachment_type.PNG)
        # 查找搜索框,输入搜索关键词
        with allure.step("输入搜索关键词"):
            self.driver.find_element(
                AppiumBy.XPATH,
                "//*[@text='搜索']").send_keys(search_key)
            image_path = self.screenshot()
            allure.attach.file(image_path, name="输入搜索关键词", attachment_type=allure.attachment_type.PNG)
        # 获取搜索结果元素的文本,完成断言
        with allure.step("获取搜索结果元素的文本,完成断言"):
            eles = self.driver.find_elements(
                AppiumBy.XPATH,
                '//*[@class="android.widget.ListView"]//*[@class="android.widget.ImageView"]/../../following-sibling::*/*[1]/*[1]')
            image_path = self.screenshot()
            allure.attach.file(image_path, name="获取搜索结果元素的文本,完成断言",
                          attachment_type=allure.attachment_type.PNG)
            assert search_key in [ele.text for ele in eles]
Java 实现
import io.appium.java_client.AppiumBy;
import io.appium.java_client.TouchAction;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.touch.offset.ElementOption;
import io.qameta.allure.Allure;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebElement;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TestMember {

    public static AndroidDriver driver;
    @BeforeAll
    public static void setUpClass() {
        //配置信息
        UiAutomator2Options uiAutomator2Options = new UiAutomator2Options()
                .setPlatformName("Android")
                .setAutomationName("uiautomator2")
                .setNoReset(true)
                .amend("appium:appPackage", "com.tencent.wework")
                .amend("appium:appActivity", ".launch.LaunchSplashActivity")
                .amend("appium:forceAppLaunch", true)
                .amend("appium:shouldTerminateApp", true);
        //初始化
        try {
            driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), uiAutomator2Options);
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
    @AfterAll
    public static void tearDownClass(){
        driver.quit();
    }

    private void ElementScreenBase(String message) throws IOException {
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        Allure.addAttachment(message, "image/png", new FileInputStream(screenshot), ".png");
    }

    /**
     * 前置条件: 登录成功
     * 1. 进入通讯录页面
     * 2. 点击搜索按钮,输入一个已经存在的成员名称。
     */
    @Test
    public void searchMember() throws IOException {

        driver.findElement(AppiumBy.xpath("//*[@text='通讯录']")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='Hufflepuff']/../../../../*[3]/*[1]")).click();
        driver.findElement(AppiumBy.xpath("//*[@text='搜索']")).sendKeys("张三");
        List<WebElement> webElements =  driver.findElements(AppiumBy.xpath("//*[@class='androidx.recyclerview.widget.RecyclerView']//*[@class='android.widget.LinearLayout']//*[@class='android.widget.TextView']"));
        //  1. 遍历所有元素 2. 获取所有元素对应的文本 3. 判断是否包含张三 4. 返回判断结果,如果所有元素都包含,则返回true
        // 到最后一个页面进行截图
        ElementScreenBase("查找成员");
        assertTrue(webElements.stream().allMatch(item -> item.getText().contains("张三")));
    }
}

总结

课后练习

  1. 编写添加成员成功和失败的测试用例,具体参考测试用例-添加联系人测试场景。
  2. 测试结果需添加截图到报告中。
  3. 观察 Appium Server/Desktop 的日志信息,了解 Appium 相关的通信原理。

相关资料

  • 环境安装与配置

Appium 环境安装

  • emulator 安装应用方法

模拟器控制