licc

心有猛虎 细嗅蔷薇

0%

iOS XCTest实战—解决国际化开发测试痛点(上)

海外项目一定离不开国际化适配,而国际化文案的展示&机型适配一直是开发和测试中的痛点。
本人负责的项目一直深耕海外,目前支持13种国际化语言包括R2L的阿拉伯语。作为订阅类App,需要进行大量的订阅页AB测试。本文主要介绍了在项目中如何利用XCTestTest PlanStoreKit Configurationxcodebuild命令、Shell脚本进行快速UI走查的。

0.国际化痛点

在国际化日常开发中,国际化文案适配是一个难题,除了开发需要考虑各种换行和边界问题等,测试同学也要花精力挨个语种和机型进行走查。尤其是订阅页面各个国家的货币展示页不相同。
下图列举了一些国际化常见的问题:

⬆️换行后依旧无法展示

⬆️文案过长,小屏幕机型展示问题

⬆️货币价格过长,需要额外适配


这些问题都需要开发自测或UI走查才能发现,下面我们一起来解决这些痛点。

1.需求梳理

开始之前先梳理一下需求,通过平时我自测的痛点以及和测试同学的沟通,如果用脚本进行测试需要达到以下效果:

  1. 所有国际化文案和主要机型都要覆盖
  2. 以页面截图为准,人工过一下,方便发现问题和报bug,最重要的是方便后期UI Debug。
  3. 对订阅功能,可以方便切换不同国家展示不同的货币
  4. 脚本要方便使用,可以方便配置不同的机型、国际化语言和货币币种,后期方便部署到Jenkins
  5. 结果呈现要分机型、iOS系统,通过截图名称可以知道是哪个页面

2.XCTest

XCTest是Xocde的原生测试框架。相对于KiwiSpectaExpecta等第三方框架更容易上手。而选择XCTest还有一个重要的原因是Apple在2019 WWDC推出的Test Plans对国际化测试非常友好。

(详见:Testing in Xcode,下文也会详细介绍)

Xcode在创建项目时候一般会自动创建Test和UITest工程,因为只需要测试UI,使用UITest即可。

具体的XCTest文档详见User Interface Tests,这里就不展开讲了。

在开始写测试脚本之前,我们需要解决以下几个问题:

i.对页面进行截图

截图方法很简单,封装成一个方法如下:

func takeAScreenshot(_ name: String) {
let screenshot = XCUIScreen.main.screenshot()
let screenshotAttachment = XCTAttachment(
uniformTypeIdentifier: "public.png",
name: "Screenshot-\(UIDevice.current.name)-\(name).png",
payload: screenshot.pngRepresentation,
userInfo: nil)

screenshotAttachment.lifetime = .keepAlways
add(screenshotAttachment)
}

还可以指定截图的质量,比如我们工程运行一次脚本需要截图500+,就需要指定低质量了。经过测试低质量的图片占用大小比高质量能压缩近20倍左右。但是需要注意,在M1芯片的Mac上,指定质量的截图方法会报错,应该是Xcode的适配问题。

func takeAScreenshot(_ name: String) {
let screenshot = XCUIScreen.main.screenshot()
let screenshotAttachment = XCTAttachment.init(screenshot: screenshot, quality: .low)
screenshotAttachment.name = "\(UIDevice.current.name)-\(name).jpeg"
screenshotAttachment.lifetime = .keepAlways
add(screenshotAttachment)
}

测试完成后,截图可以在Xcode中查看

ii. 国际化适配

在XCTest中,我们可以模拟用户的操作,比如点击一个按钮,点击一个Label等,而找到控件的方式一般是按Button文本或者Label的Text.
比如我页面有一个Button,title是“testButton”

func testExample() throws {
// UI tests must launch the application that they test.
let btn = app.buttons["testButton"]
XCTAssertTrue(btn.exists)
//tap btn
btn.tap()
sleep(2)
takeAScreenshot("button")
}

但是当我开启国际化后,按钮文案是跟随系统语言变化的,那上述代码就会执行失败。

解决办法是为控件设置accessibilityIdentifier属性。accessibilityIdentifier是专为UITest设计的。可以方便的找到需要的元素。

//为btn设置accessibilityIdentifier
btn.accessibilityIdentifier = "myTestButton"

//在XCTest中 按accessibilityIdentifier查找
let btn = app.buttons["myTestButton"]

iii. 主App开启测试环境

XCTest每次运行都会重新运行App,一般主App启动入口会有很多业务逻辑,对测试会造成影响。我们需要判断本次启动是从XCTest启动的,这样可以开启测试环境,去掉一些无关的逻辑。
在UITest中可以向launchArguments中加入自定义参数。

class UITestDemoUITests: XCTestCase {
var app: XCUIApplication!

override func setUpWithError() throws {

continueAfterFailure = false
app = XCUIApplication()
//向主App传递参数 也可以写在测试方法中
app.launchArguments.append("UDUIXCTestConfig#SubscribePage")
app.launch()
}
}

AppdelegatedidFinishLaunchingWithOptions中判断,可以指定根据字符串指定需要测试的场景。

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
...
for subArgue: String in CommandLine.arguments {
if subArgue.hasPrefix("UDUIXCTestConfig") {
//开启测试模式
UDXCTestManager.shareInstance().argument = subArgue
UDXCTestManager.shareInstance().testModeOn = true
break
}
}
...
}

iv. Demo

开启上述步骤后,已经可以初步进行UI测试了,通过设置测试环境,确保启动后进入待测试页面。合理利用for循环以及截图。需要注意接入前如果涉及页面跳转等,要用sleep()函数等待页面渲染完后再截图

比如我们工程中一个测试方法

func testSubscribePage() throws {


if #available(iOS 14.0, *) {
let session = try? SKTestSession.init(configurationFileNamed: "Configuration")
session?.disableDialogs = true
session?.clearTransactions()
} else {

}

app.launchArguments.append("UDUIXCTestConfig#SubscribePage")
app.launch()

let TypeList = ["S", "T", "U", "V", "V3"]
for sub in TypeList {
let typeElement = app.tables.staticTexts[sub]
XCTAssertTrue(typeElement.exists)
typeElement.tap()
sleep(2)
self.takeAScreenshot("\(sub) List")

//测试子页面
let list = ["weekly_free", "monthly_free", "yearly_free"]
for sublist in list {
sleep(2)
let subtypeElement = app.tables.staticTexts[sublist]
XCTAssertTrue(subtypeElement.exists)
//进入待测试页
subtypeElement.tap()
sleep(2)
self.takeAScreenshot("\(sub)-\(sublist)-1")
sleep(1)
if sub == "S" || sub == "T" {
//滚动到底部
app.swipeUp()
sleep(1)
//对底部截图 无法滚动的页面可以考虑过滤掉
self.takeAScreenshot("\(sub)-\(sublist)-2")
sleep(3)
} else {
sleep(4)
}

}

//返回上一页面
let backButton = XCUIApplication().navigationBars.buttons["backButton"]
XCTAssert(backButton.exists)
backButton.tap()
sleep(2)

}

}

3.TestPlans

上述方案每次只能测试一个国际化语言,每次切换语言后需要重新运行。在TestPlans推出之前需要我们自己写xcodebuild命令来指定国际化语言或者在Schemes中配置App Langauge

TestPlans是Apple在2019年WWDC推出的测试工具,详细信息详见《Testing in Xcode》, 推出后可以更方便的进行国际化语言以及其余配置的切换。

开启方式也很简单,在“Edit Scheme..”—> “Test” —> “Covert to use Test Plans”


选择放到主工程中,设置成Default

i. 配置Configuration

在主工程中找到.xctestplan后缀的文件。

可以发现在Configuration中可以设置语言、地区、开启Code Coverage等功能。

可以点击加号加新的配置文件,默认的配置是读取Share Settings的配置。

比如我们的工程,配置了主要适配的语言。

ii. 选择测试方法、开启并行

在Test面板,可以看到所有测试方法,可以勾选需要测试的方法,勾选多个会按着从上到下的顺序执行。在Options中打开Execute in parallel。打开后可以在资源允许的情况下,开启多个克隆的模拟器,提高测试速度。

比如执行Phone X机型的一个测试方法,在Configuration中配置了6个配置文件,资源允许情况下会开启3个iPhone X模拟器,每个模拟器跑2个配置,速度提高了3倍。

iii. 运行Test Plans

Test Plans的运行方式和之前一样,找到之前的方法入口,点击运行或者按右键只运行部分配置

iv. 查看结果

Test Plans运行完后可以在Xcode中查看结果,如果运行多个配置,每个配置的结果都会单独呈现

国际化测试需要注意,在Configuration中有Localization Screenshots选型,默认是On,只要本页面文件进行了国际化(使用NSLocalizedString)都会自动截图,如不需要可以关闭。

到目前为止,我们得到了一个初步的测试方案,可以用XCTest写完页面截图逻辑后,使用Test Plans批量运行多个语言。下一章会介绍如何解决订阅中多币种的适配、如何快速导出测试截图以及最终用xcodebuild和shell脚本自动化所有操作。

4.iOS XCTest实战—解决国际化开发测试痛点(下)

下篇详见:iOS XCTest实战—解决国际化开发测试痛点(下)