随着 SwiftUI 的使用,使得现在写 UI Test 也更为容易了一些,一般的视图都能够被分离出来,单独拿出来进行测试。如果再加上 Page Object Model,那么将能够给写 UI Test 锦上添花。下面就来一探究竟吧。
本文的示例代码来自于 UI Testing using Page Object pattern in Swift
例子使用一个简单的登录界面,大家可以把它简单地想象成这样子:
基本的测试写法
我们希望测试这个界面的界面元素和登录交互,代码可以长这个样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final class LoginTests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["testing"]
app.launch()
}
func testLoginFlow() {
let email = app.textFields["email"]
email.tap()
email.typeText("cmecid@gmail.com")
let pwd = app.secureTextFields["password"]
pwd.tap()
pwd.typeText("pwd")
app.buttons["login"].tap()
let message = app.staticTexts["Hello World!"]
XCTAssertTrue(message.waitForExistence(timeout: 5))
}
}
在测试 case 执行前,获取到 app 实例,然后启动 app。在测试方法中,通过查找界面元素,获取到输入 email 和 password 的 TextField
,并且点击他们输入测试文本,然后找到 login 的 Button
点击它,期望是在按钮点击之后,切换到一个新的界面,显示 Hello World! 的文本。这是一个基本的登录操作的流程。
上面的测试代码,是有可优化的点的。比如 setUp
方法中的 app 环境,我们几乎在每个 UI 测试中都会用到它,因此它是可以被抽取的;还有假使这个登录流程需要作为某一个 UI 测试的前置操作,会被复用到,那么势必会出现重复代码。下面就通过两次优化,来引出 Page Object Model。
抽取 UI Test 的基类
通过把 setUp
和 tearDown
的代码抽取出来,让子类继承,来简化每次测试前后的代码书写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UITestCase: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["testing"]
app.launch()
}
override func tearDown() {
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .deleteOnSuccess
add(attachment)
app.terminate()
}
}
在 setUp
阶段启动 app,在 tearDown
阶段,如果有测试失败的话,保存失败场景的截图,便于 Debug。
通过继承 UITestCase
,LoginTests
可以简化为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class LoginTests: UITestCase {
func testLoginFlow() {
let email = app.textFields["email"]
email.tap()
email.typeText("cmecid@gmail.com")
let pwd = app.secureTextFields["password"]
pwd.tap()
pwd.typeText("pwd")
app.switches["rememberMe"].tap()
app.buttons["login"].doubleTap()
app.buttons["login"].twoFingerTap()
let message = app.staticTexts["Hello World!"]
XCTAssertTrue(message.waitForExistence(timeout: 5))
}
}
使用 Page Object Model
Page Object 是按照界面来进行封装,封装的类只提供当前界面的一些元素和交互方法,而测试用例来复杂消费他们,然后构建整个交互流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protocol Screen {
var app: XCUIApplication { get }
}
struct LoginScreen: Screen {
let app: XCUIApplication
private enum Identifiers {
static let email = "email"
static let password = "password"
static let login = "login"
static let error = "error"
}
func typeEmail(_ email1: String) -> Self {
let email = app.textFields[Identifiers.email]
email.tap()
email.typeText(email1)
return self
}
func tapLoginExpectingError() -> Self {
app.buttons[Identifiers.login].tap()
let error = app.staticTexts[Identifiers.error]
XCTAssertTrue(error.waitForExistence(timeout: 5))
return self
}
func tapLogin() -> MessageScreen {
app.buttons[Identifiers.login].tap()
return MessageScreen(app: app)
}
func typePassword(_ password: String) -> Self {
let pwd = app.secureTextFields[Identifiers.password]
pwd.tap()
pwd.typeText(password)
return self
}
}
LoginScreen
这个结构体,只包含登录界面的元素和一些交互方法,它把每个交互都单独定义了方法,这样不论测试用例想如何进行验证,都是比较灵活的。然后我们还有一个登录成功之后的 MessageScreen
。
1
2
3
4
5
6
7
8
9
struct MessageScreen: Screen {
let app: XCUIApplication
func verifyMessage(_ message: String) -> Self {
let message = app.staticTexts[message]
XCTAssertTrue(message.waitForExistence(timeout: 5))
return self
}
}
以上所有的方法,都返回了自己或者其他的 Screen
,这样方便我们进行链式调用来组合一些元素和交互,例如 tapLogin
方法返回了 MessageScreen
,这也是符合交互逻辑的。正常情况下,点击完登录应该跳转到 Message 界面。
最终我们的 UI Test 就变成了这样:
1
2
3
4
5
6
7
8
9
10
11
12
final class LoginTests: UITestCase {
func testLoginFlow() {
LoginScreen(app: app)
.typeEmail("Cmecid@gmail.com")
.typePassword("password")
.tapLoginExpectingError()
.typeEmail("Cmecid@gmail.com")
.typePassword("pwd")
.tapLogin()
.verifyMessage("Hello World!")
}
}
这样的调用逻辑是非常清晰的,并且各个页面都能被复用,每个页面里面的具体细节都被隐藏了起来。
例子中使用的是 Screen 而非 Page,这是因为 Page Object 这个概念是从 Web 来的,所以示例的作者使用了 Screen 代替了 Page,其实在项目实践中,并不一定每一个类都要代表一个 Screen,它可以是对一个 UI Component 的测试,因为我们往往会把一些可复用组件进行拆分,所以对小组件单独进行测试也是不错的实践。
是否在 Page 中进行断言
大家会发现,在文中的示例其实是在 Page 的方法中加入了断言语句,而在测试用例中只是调用了相应的方法。还有一种观点是 Page 应该只负责提供界面元素和交互方法,而不加断言,让测试方法自己去决定想要怎么断言。Martin Fowler 在他的文章中指出了这两种方法的优缺点:
- 在 Page 中写断言,优点是可以避免重复的断言语句,提供更符合上下文的错误信息,以及更符合 TellDontAsk 风格的 API。
- 而缺点则是,使得 Page 的职责不再单一,混杂了断言语句的逻辑,会使得 Page 变得更大更复杂。
因为示例的场景比较简单,因此看起来在 Page 中写断言还是挺清晰的,但是随着页面和逻辑变得更复杂,它的弊端会不会展现出来呢?
Martin Fowler 个人更偏向于不在 Page 中写断言,大家可以根据自己的想法和项目情况,来进行选择。目前个人觉得在页面不复杂的情况下,在 Page 中写断言是更清晰的。
Comments powered by Disqus.