从0到1构建移动端应用自动化测试

1,324 阅读6分钟

背景

公司主业务是做跨境电商的,每次发版本都需要回归测试。大部分固定的业务逻辑没怎么变动,但是耗时耗力。由此,今年我们尝试构建自动化测试。

环境搭建

项目需要集成Appium环境,如果需要在本机执行自动化测试,需要安装Appium相关的环境。安装可以通过appium-doctor来检测appium是否正确配置。目前移动端的环境配置如下:

  1. iOS环境配置:
  • Xcode
  • Carthage (可使用brew安装, brew install Carthage)
  • Node iOS检测环境是否配置成功(必须配置项目):
➜  ~ appium-doctor --ios
info AppiumDoctor Appium Doctor v.1.16.0
info AppiumDoctor ### Diagnostic for necessary dependencies starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: /usr/local/bin/node
info AppiumDoctor  ✔ Node version is 14.17.0
info AppiumDoctor  ✔ Xcode is installed at: /Applications/Xcode.app/Contents/Developer
info AppiumDoctor  ✔ Xcode Command Line Tools are installed in: /Applications/Xcode.app/Contents/Developer
info AppiumDoctor  ✔ DevToolsSecurity is enabled.
info AppiumDoctor  ✔ The Authorization DB is set up properly.
info AppiumDoctor  ✔ Carthage was found at: /usr/local/bin/carthage. Installed version is: 0.36.0
info AppiumDoctor  ✔ HOME is set to: /Users/taohongyu
info AppiumDoctor ### Diagnostic for necessary dependencies completed, no fix needed. ###
  1. 安卓环境配置:
  • AndroidStudio
  • ANDROID_HOME(环境变量配置)
  • JAVA_HOME(环境变量配置)

安卓检测是否配置成功:

➜  ~ appium-doctor --android
info AppiumDoctor Appium Doctor v.1.16.0
info AppiumDoctor ### Diagnostic for necessary dependencies starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: /usr/local/bin/node
info AppiumDoctor  ✔ Node version is 14.17.0
info AppiumDoctor  ✔ ANDROID_HOME is set to: /Users/taohongyu/Library/Android/sdk
info AppiumDoctor  ✔ JAVA_HOME is set to: /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home
info AppiumDoctor    Checking adb, android, emulator
info AppiumDoctor      'adb' is in /Users/taohongyu/Library/Android/sdk/platform-tools/adb
info AppiumDoctor      'android' is in /Users/taohongyu/Library/Android/sdk/tools/android
info AppiumDoctor      'emulator' is in /Users/taohongyu/Library/Android/sdk/emulator/emulator
info AppiumDoctor  ✔ adb, android, emulator exist: /Users/taohongyu/Library/Android/sdk
info AppiumDoctor  ✔ 'bin' subfolder exists under '/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home'
info AppiumDoctor ### Diagnostic for necessary dependencies completed, no fix needed. ###
  1. Appium环境配置:

具体配置过程可参考: Setup Appium on MacOS for Android and iOS Automation Appium Big Sur Update | Medium

编写测试用例

Appium 的核心理念之一是,你不应该为了测试而改变被测的应用程序。出自:appium.io/docs/cn/wri…

我们编写用例,其实就是编写单元测试代码。一个用例即一个单元或一个方法;因此,它的输入与输出是独立的。我在实现过程中,选择将应用的底部Tab作为用例的起点与终点。简单说,一条用例的执行是从底部的Tab开始,执行结束后,返回至底部Tab。

比如:选择地址的用例。 操作步骤: 1.进入Account,选择切换国家至”Aruba”。 2.进入购物车,添加商品,点击下单。

结果: 地址显示正确。

@Test
fun selectAddress() {
    val case = allCases.case(PlaceOrderTestCaseIds.SelectAdrress)
    /// 切换至未知国家, 选择AccountTab
    tabbarActor.selectAt(LITBTab.Account)
    accountActor.changeCountry("Aruba")

    tabbarActor.selectAt(LITBTab.Cart)
    cartActor.deleteAll()
    cartActor.onRecommendationProductAddToCart()
    cartActor.onCheckoutToPreOrder()
    val isLegal = checkoutActor.isSelectedAddressLegal()
	  /// 返回值Tab页
    checkoutActor.backToRoot()
    assertTestCase(case, isLegal, Error("选择了不可用的地址!"))
}

App是以底部Tabbar为主的页面结构,每个Tab具有导航栏的结构;因此,在进行页面操作时,对底部的Tab和导航栏功能进行了封装。

Tabbar结构选中要操作的某个Tab

class TabbarActor: Actor {
	private val tabbarItems get() = driver.findViewsById(HomeViewId.TabBar)

...
	//// 选中某个Tab
	fun selectAt(tab: LITBTab) {
    val isSelected = tabbarItemsSelected[tab.index]?:false
    var sleep: SleepDuration = SleepDuration.Regular
    if (!isSelected) {
        sleep = SleepDuration.Medium
    }

    if (Emulator.isAndroid()) {
        /// 购物车Tab调取接口较多,选中时,应给足时长。
        if (tab == LITBTab.Cart || tab == LITBTab.Account) {
            tabbarItems?.elementAtOrNull(tab.index)?.clickThen(SleepDuration.Medium)
        } else {
            tabbarItems?.elementAtOrNull(tab.index)?.clickThen(sleep)
        }
    } else {
        val tabs = listOf<String>(HomeViewId.tabHome, HomeViewId.tabCategory, HomeViewId.tabCommunity, HomeViewId.tabCart, HomeViewId.tabAccount)
        driver.clickBy(tabs.elementAtOrNull(tab.index)?:"", sleep.seconds)
    }

    /// 存储 当前选中的状态
    tabbarItemsSelected[tab.index] = true
	}
}

导航栏返回操作

open class NavigationActor: Actor {

    private val back: MobileElement?
        get() = driver.findViewById(NavigationViewId.Back)
    private val ivBack: MobileElement?
        get() = driver.findViewById(NavigationViewId.IVBack)
    private val grayBack: MobileElement?
        get() = driver.findViewById(NavigationViewId.GrayBack)

    private val backBtn get() = driver.findViewById(NavigationViewId.BackBTN)
    private val whiteBack get() = driver.findViewById(NavigationViewId.WhiteBack)

    fun goNavigationBack(): Boolean {
        val all = mutableListOf(back, ivBack, grayBack, backBtn, whiteBack)
        val backItems =  all.filterNotNull()
        if (backItems.isNotEmpty()) {
            backItems.firstOrNull()?.clickThen()
            return true
        } else {
            return false
        }
    }

	/*
	* 返回至最底部*/
  fun backToRoot() {
      while (goNavigationBack()) {
          println("返回按钮点击!")
      }
      return
  }
}

每一步操作都需要时间,尤其是涉及网络请求的交互。因此,我们的操作步骤(click,setValue等事件)每执行一次,就需要让线程睡眠几秒或几十秒,等待视图创建或请求的返回。


/*通过Name - 点击*/
fun <MobileElement : WebElement?> AppiumDriver<MobileElement>.clickByName(name: String, sleep: Int = SleepDuration.Regular.seconds): MobileElement? {
    val element: MobileElement? = this.findViewByName(name)
    element?.click()
	  /// 线程睡眠
    Util.sleep(sleep)
    return element
}

public class Util {
    /**
     * 线程等待timeSecond 秒
     */
    public static void sleep(int timeSecond) {
        try {
            Thread.sleep(timeSecond * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
   }
}

除去通用的操作,剩余的操作都是与测试的业务相关。但基本每条测试用例都是这样的一个执行过程:入口(选择某个Tab) - 用例执行步骤 - 获取到期望值 - 返回到最底部 - 断言结果。这里有个细节,返回到最底部需要在断言结果前执行;是因为,每条用例执行完后应该尽可能的不影响App的状态(如:当前处在哪个业务页面,登入与登录状态等等)。这样是便于下一条用例的执行。

Jenkins一键构建

我们期望自动化的用例能够通过Jenkins部署在服务器上,固定时段自动执行。每天只需查看结果和报告。因此我们通过Jenkins的日程表定时构建,配置了日程表,项目即定时执行了。

截屏2021-10-31 14.52.57.png 当我们在运行Appium时,需要配置DesiredCapabilities。这样Appium才能知道,此时使用哪个测试框架,测试哪个App。DesiredCapabilities必需的四个参数automationNameplatformNameplatformVersiondeviceName。 为了能一键构建,在Jenkins上配置了个可选参数Platform,最后在Build的时候,依据当前Platform不同,生成不同的配置文件,最后在执行测试。 安卓平台生成配置文件的Shell脚本:

# 0.初始化配置
# ${Platform} 参数从Jenkins 读取
rm ./src/main/cmd/Platform.yml
rm ./*.out
echo "当前配置文件目录如下:"
ls ./src/main/cmd/

echo "正在执行的配置文件目录如下:"
cp ./src/main/cmd/Platform_android.yml ./src/main/cmd/Platform.yml
ls ./src/main/cmd/

# 读取Jenkins Job的配置
echo "buildNumber: ${BUILD_NUMBER}
gitBranch: ${GIT_BRANCH}
enableWebhook: ${EnableWebhook}
webhookURL: ${WebhookURL}"> src/main/cmd/jenkins_configure.yml

# 复制打包好的apk文件至指定测试目录
android_apk_path="UI_Android_Packing/litb/build/outputs/apk/**/**/*.apk"
cp ~/Downloads/workspace/${android_apk_path} src/test/resources/xxxx.apk

# 1.启动Appium
appium &

# 2.启动模拟器
# 进入emulator目录
$ANDROID_HOME/emulator/emulator -avd 'Pixel_XL_API_31'

执行用例测试Shell脚本

nohup ./auto_start_android.sh &
sleep 30

./gradlew cleanProject
./gradlew universalTest

至此,我们便完成了Jenkins的一键构建与自动构建。

棘手问题

  1. 滑动手势 有时候程序当前页面所展示的元素需要滑动才显示出来时,就需要模拟滑动的操作。官网上所提到的Swipe操作与Scroll操作均 使用起来不是特别顺手。最后在Appium的GitHub issue里找到了通过Actions API,来模拟滑动完美的解决了问题。并对AppiumDriver进行了滑动操作的封装。
/*
* 垂直方向上的scroll
* */
fun AppiumDriver<MobileElement>.verticalScrollToFind(scrollView: MobileElement?, id: String, yOffset: Int, maxOffset: Int, extented: Boolean = false): List<MobileElement>? {
    if (scrollView == null) {
        return null
    }

    var driverYOffset = yOffset
    var driverMaxOffset = maxOffset

    if (this is IOSDriver) {
        driverYOffset = yOffset / 10
        driverMaxOffset = maxOffset / 10
    }

    var f: Int = 0
    var v: List<MobileElement>? = mutableListOf()
    while ((driverMaxOffset < 0 && f > driverMaxOffset) || (driverMaxOffset > 0 && f < driverMaxOffset)) {
        v = findViewsById(id)
        if (!v.isNullOrEmpty()) {
            if (extented) {
                /// 返回前,多执行一次滚动,防止被遮挡
                Actions(this).clickAndHold(scrollView).moveByOffset(0, driverYOffset).perform()
            }
            return v
        }
        Actions(this).clickAndHold(scrollView).moveByOffset(0, driverYOffset).perform()
        f += driverYOffset
        Util.sleep(SleepDuration.Regular.seconds)
    }
    if (extented) {
        /// 返回前,多执行一次滚动,防止被遮挡
        Actions(this).clickAndHold(scrollView).moveByOffset(0, driverYOffset).perform()
    }
    return v
}

  1. 键盘操作 Appium提供的键盘API实测未响应。目前仅通过executeScript的方式来执行键盘操作。如搜索按钮。
/*
* 键盘搜索按钮*/
fun <MobileElement : WebElement?> AppiumDriver<MobileElement>.KeyboardSearch(sleep: Int = 5) {
    if (this is AndroidDriver<MobileElement>) {
        this.executeScript("mobile: performEditorAction", mutableMapOf("action" to "search"))
        Util.sleep(sleep)
    } else {
        this.clickByName("Search")
        Util.sleep(sleep)
    }
}

项目基于Appium,使用TestNG和Jenkins完成了自动化构建,实现了:

  • 跨平台(目前仅Android与iOS)
  • Jenkins 一键构建,参数化构建。
  • 日志企业微信推送与Jenkins报告
  • App端的部分测试用例已采用自动构建测试

最后,项目通过Jenkins在服务器上默默的执行;在企业微信群和Jenkins上里查看报告就完了。

F7363DD7-67F2-4D7A-B8BF-B88E23ADE344.png 企业微信群的报告(某个模块)

3EB1AC92-A552-4A2B-AC26-19A7A7B67B71.png Jenkins上的TestNG 报告(部分截图)