返回博客首页
← 所有文章

正确开始测试您的 NativeScript 应用

2018年3月6日 — 作者:Jérémy Pelé

这是一篇由 Jérémy Pelé 撰写的客座文章

Chronogolf,我们已经使用 NativeScript 18 个月了,我一直对缺乏合适的端到端测试 (e2e) 解决方案感到沮丧。

随着我们的移动应用用户群迅速增长,测试变得至关重要,需要得到妥善解决。由于应用商店的限制,每个推送到生产环境的错误都需要在 2-3 天内解决。您最好确保您推送到生产环境的代码是正确的!

我们首先尝试了 functional-tests-core,它在某种程度上有效,但配置繁重,规范编写起来很痛苦。然后我们转向了 nativescript-dev-appium,这是一个好的开始,但在当时还没有完全准备好(我们说的是 2017 年夏季开始)。

NativeScript 生态系统的一件好事:一切都在快速发展!我将向您展示一个完整的流程,用于使用 e2e 规范来覆盖您的应用。

failing test

测试您的应用

我再次尝试了 nativescript-dev-appium 插件,并且很高兴地看到它现在比以前完整得多。

  • 现在支持 TypeScript 语法。
  • 支持 asyncawait
  • 访问 webdriver_io
  • 简化了选择器
  • 图像比较

首先,安装 nativescript-dev-appium 及其依赖项。

1) 安装

npm install -g appium
npm install -D nativescript-dev-appium

您会发现一个生成的 e2e 文件夹。此文件夹将包含您的第一个规范文件以及 Appium 所需的配置文件夹。

2) 配置

您需要定义将用于运行套件的设备。

请注意,当在测试中使用图像比较时,每个设备都需要获取自己的一组资产(因为屏幕大小、填充等可能会根据屏幕分辨率而有所不同)。根据您的存储方式,这可能难以维护,并且会增加您的仓库大小。

为了简单起见,我们选择使用一个模拟器 (iOS) 和一个模拟器 (Android)。

打开 e2e/config/appium.capabilities.json 并定义您的设置。

{
  "sim.iPhone7": {
    "platformName": "iOS",
    "platformVersion": "11.2",
    "deviceName": "iPhone 7",
    "fullReset": true, // Device will be destroyed and fresh install will be made after each suite
    "app": ""
  }
}

要使用上述功能配置运行套件,您将使用以下命令

npm run e2e -- --runType sim.iPhone7 --reuseDevice

现在让我们开始编写一些测试...

驱动程序

每个套件都将按照相同的模式编写:需要初始化一个新的 AppiumDriver 单例,所有测试都将依赖于它。

describe('My Suite', () => {
  let driver: AppiumDriver

  before(async () => {
    // Wait for the driver instance to be created
    driver = await createDriver()
  })

  after(async () => {
   // Destroy the driver instance
   await driver.quit()
  })

  it('validates something', async () => {
    // write your test in here
  })
})

选择器

给定一个简单的按钮,我将向您展示如何使用不同的选择器类型来选择它。

<Button automationText="loginFormSubmit"
    class="submit btn-primary btn-rounded-sm"
    text="Submit" (tap)="onSubmit()">
</Button>

XPath (findElementByXPath, findElementsByXPath)

let submitBtn
if (driver.isAndroid) {
  submitBtn = await driver.findElementByXPath('//android.widget.Button')
} else {
  submitBtn = await driver.findElementByXPath('//XCUIElementTypeButton')  // ios 11
  submitBtn = await driver.findElementByXPath('//UIAButton') // ios
}

优点

  • 默认情况下被大型社区使用

缺点

  • iOS 和 Android 的路径不同。iOS 11 更新了其组件前缀,因此您也需要处理它。
  • 难以编写和阅读。如果更新模板或组件,则可能需要更新规范(因为路径也可能发生变化)。

    // 您还可以使用相对于平台抽象映射的驱动程序定位器 let submitBtn = await driver.findElementByXPath(//${driver.locators.button})

文本 (findElementByText, findElementsByText) - 包含/完全匹配

const submitBtn = await driver.findElementByText('Submit', SearchOptions.exact)
await submitBtn.click()

优点

  • 无需更新您的应用,您可以直接开始编写规范。

缺点

  • 如果您的值发生变化怎么办?通常您不希望选择器依赖于文本。

组件类型 (findElementByClassName, findElementsByClassName) - 目标 NativeScript 组件:标签、按钮、网格布局...

  • 缺点:过于通用,随着布局随时间推移而变化,无法扩展。

    // 直接使用类名类型 const submitBtn = await driver.findElementByClassName('button') await submitBtn.click()

    // 使用驱动程序定位器辅助函数的替代方法 const submitBtn = await driver.findElementByClassName(driver.locators.button) await submitBtn.click()

注意:如果驱动程序在页面上找到多个按钮,则将选择第一个按钮。您可以将页面上的所有按钮选择为 UIElement 数组,然后根据其索引访问所需的按钮。

const submitBtns = await driver.findElementsByClassName('button')
await submitBtns[0].click()

辅助功能 ID (findElementByAccessibilityId, findElementsByAccessibilityId)

  • 随着时间的推移,这是最佳选择,因为它不依赖于您的 XML 结构或值/翻译。
  • 在“可交互节点”上放置 automationText="loginPasswordInput"(因为 automationText 是辅助功能,自定义组件将不起作用。您需要将该选项作为输入传递,它将放置在自定义组件模板的根目录上)。
  • 优点

    • 能够选择输入、标签、整个组件
    • 易于维护且对代码演变具有鲁棒性

    const submitBtn = await driver.findElementByAccessibilityId('loginFormSubmit') await submitBtn.click()

规范类型

主要有两种类型的规范

基于图像比较

屏幕截图

it('compare screen', async () => {
  assert.isTrue(await driver.compareScreen('my-whole-screen'))
})

元素

it('compare button element', async () => {
  const submitBtn = await driver.findElementByAccessibilityId('loginFormSubmit')
  assert.isTrue(await driver.compareElement(submitBtn, 'my-submit-btn'))
})

基于元素

文本值

it('compare button element', async () => {
  const submitBtn = await driver.findElementByAccessibilityId('loginFormSubmit')
  const submitBtnText = submitBtn.text()
  assert.isEqual(submitBtnText, 'Submit')
})

组件显示/隐藏?

it('compare button element', async () => {
  const submitBtn = await driver.findElementByAccessibilityId('loginFormSubmit')
  assert.isTrue(await submitBtn.isDisplayed())
})

完整的示例规范请参见此处。

太棒了!您可以开始在本地测试您的应用了 :)

npm run e2e -- --runType sim.iPhone7 --reuseDevice

test running

自动化您的流程

CircleCI

我们如何自动化此流程?我们将使用持续集成工具(此处为 CircleCI),而不是手动构建和触发套件。

CircleCI 提供 macOS 虚拟机,这意味着我们将能够使用它们在云中构建和测试我们的应用。您需要将您的 Git 仓库连接到 CircleCi 帐户,其余的只需要配置。

您需要在仓库的根目录中放置一个 circle.yml 文件。此文件将包含需要在虚拟机上执行的不同执行。在下面的示例中,我们希望配置一台具有所有依赖项的新机器,构建应用并在其上运行套件。

machine:
  xcode:
    version: 9.0.1
dependencies:
  cache_directories:
    - ~/.npm
    - ~/Library/Caches/Homebrew
    - ~/Library/Caches/CocoaPods
  pre:
    # Fetch cocoapods specs from S3 instead of github
    - curl -sS https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash
    - npm i -g nativescript --ignore-scripts
  override:
    - npm set progress=false && npm install
  post:
    # Pre-start emulator you'll use for the specs
    - xcrun instruments -w "iPhone 7 (11.0.1) [":
        background: true
compile:
  override:
    # Build Test App
    - tns prepare ios || echo "ios prepare"
    - tns build ios
test:
  override:
    # E2E
    - npm run e2e -- --runType sim.iPhone7
  post:
    # Export results as artifacts downloadable
    - mv e2e/reports/**/* $CIRCLE_ARTIFACTS/
general:
  branches:
    only: # list of branches to listen to
      - master
      - develop

由于您始终从一个全新的环境进行测试,因此您可以确保从头开始构建一个干净的应用(没有遗留的旧的转译后的 .js 文件,没有遗漏的文件等)。

如果要使用本地服务器测试应用,可以对其进行配置并在同一虚拟机上启动它。只需创建您自己的序列并将其添加到上面的示例中即可。

云替代方案

NativeScript 建议使用 Travis CI 测试您的插件。它可能是 Circle CI 的一个不错的替代方案。

我个人没有使用过它,但您可以看看 nativescript-facebook 是如何做的。

您还可以选择一个不提供模拟器/模拟器的持续集成系统,并将其与 Sauce Labs 服务结合使用。

Sauce Labs 允许您将应用上传到其平台,并使用其云模拟器运行测试套件。

nativescript-dev-appium 在运行套件规范时使用 --sauceLab 选项支持 Sauce Labs。

1) 配置

{
  "local.sim.iPhone7": {
    "platformName": "iOS",
    "platformVersion": "11.2",
    "deviceName": "iPhone 7",
    "fullReset": true,
    "app": ""
  },
  "sauce.sim.iPhone7": {
    "platformName": "iOS",
    "platformVersion": "11.2",
    "deviceName": "iPhone 7 Simulator",
    "appium-version": "1.7.2",
    "app": ""
  }
}

功能以 localsauce 为前缀,以保持我们正在使用什么的清晰上下文。这显然是可选的,您可以选择此处要使用的模式。

请注意添加了 "appium-version": "1.7.2"

由于 Sauce Labs 可能使用与本地机器模拟器/模拟器不同的设备名称,请参考 Sauce Labs 平台配置器

https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/

构建您的应用并将其打包为归档文件

tns build ios && zip -r mobileapp.zip platforms/ios/build/emulator/myapp.app

将其导出到 Sauce Labs 存储

curl -v -u $SAUCE_USER:$SAUCE_KEY -X POST "http://saucelabs.com/rest/v1/storage/$SAUCE_USER/myapp.zip?overwrite=true" -H "Content-Type: application/octet-stream" --data-binary @myapp.zip

在您刚刚上传的 Sauce Labs 归档文件上运行套件

npm run e2e -- --runType sauce.sim.iPhone7 --sauceLab --appPath myapp.zip

如果需要在 Sauce Labs 上使用 API 服务器测试我的应用怎么办?

Sauce Labs 提供了一个名为 Sauce Connect 的工具,该工具在运行该工具的机器和测试应用的 Sauce Lab 虚拟机之间创建隧道。

这意味着 Sauce Labs 虚拟机将能够访问 http://localhost:3000(或您用于运行服务器的任何地址)。