iOS自動化測試的那些乾貨
前言
如果有測試大佬發現內容不對,歡迎指正,我會及時修改。
大多數的iOS App(沒有持續整合)迭代流程是這樣的
也就是說,測試是釋出之前的最後一道關卡。如果bug不能在測試中發現,那麼bug就會抵達使用者,所以測試的完整性和可靠性十分重要。
目前,大多數App還停留在人工測試階段,人工測試投入的成本最低,能夠保證核心功能的使用,而且測試人員不需要會寫程式碼。
但是,在很多測試場景下,人工測試的效率太低,容易出錯。舉兩個常見的例子:
-
一個App的核心功能,在每一次釋出版本前的測試必定會跑一遍所有的測試用例,不管對應的業務在當前版本有沒有變化(天知道開發在做業務A的時候,對業務B有沒有影響),如果這次測出新的bug,測試人員在下一次發版測試中,又不得不做這些重複的工作。
-
開發在寫API請求相關程式碼的時候沒有做資料容錯,測試在人工測試的時候都是正常的資料,所以測試通過。上線了之後,後臺配置資料的時候出了點小問題,導致大面積崩潰,boom~。
然後,老闆就要過來找你了
本文所講解的均是基於XCode 8.2.1,有些概念可能不適用於低版本的XCode
自動化測試
自動化測試就是寫一些測試程式碼,用程式碼代替人工去完成模組和業務的測試。
其實不管是開發還是測試,如果你在不斷的做重複性工作的時候,就應該問自己一個問題:是不是有更高效的辦法?
自動化測試有很多優點:
-
測試速度快,避免重複性的工作
-
避免regression,讓開發更有信心去修改和重構程式碼(個人認為最大的優點)
-
具有一致性。
-
有了自動化測試,持續整合(CI)會變得更可靠。
-
迫使開發人員寫出更高質量的程式碼。(自動化測試不通過,程式碼不允許合併)
當然,自動化測試也有一些缺點。
-
開發和維護成本高。
-
不能完全替代人工測試。
-
無法完全保證測試的準確性 - 讓程式碼去判斷一段邏輯是否正確很容易,但是,讓程式碼判斷一個控制元件顯示是否正確卻沒那麼容易。
所以,在做自動化測試之前,首先要問自己幾個問題?
-
這個測試業務的變動是否頻繁?
-
這個測試業務是否屬於核心功能?
-
編寫測試程式碼的成本有多少?
-
自動化測試能保證測試結果的準確麼?
通常,我們會選擇那些業務穩定,需要頻繁測試的部分來編寫自動化測試指令碼,其餘的採用人工測試,人工測試仍然是iOS App開發中不可缺少的一部分。
測試種類
從是否接觸原始碼的角度來分類:測試分為黑盒和白盒(灰盒就是黑盒白盒結合,這裡不做討論)。
白盒測試的時候,測試人員是可以直接接觸待測試App的原始碼的。白盒測試更多的是單元測試,測試人員針對各個單元進行各種可能的輸入分析,然後測試其輸出。白盒測試的測試程式碼通常由iOS開發編寫。
黑盒測試。黑盒測試的時候,測試人員不需要接觸原始碼。是從App層面對其行為以及UI的正確性進行驗證,黑盒測試由iOS測試完成。
從業務的層次上來說,測試金字塔如圖:
而iOS測試通常只有以下兩個層次:
-
Unit,單元測試,保證每一個類能夠正常工作
-
UI,UI測試,也叫做整合測試,從業務層的角度保證各個業務可以正常工作。
框架選擇
囉裡八嗦講的這麼多,自動化測試的效率怎麼樣,關鍵還是在測試框架上。那麼,如何選擇測試框架呢?框架可以分為兩大類:XCode內建的和三方庫。
選擇框架的時候有幾個方面要考慮
-
測試程式碼編寫的成本
-
是否可調式
-
框架的穩定性
-
測試報告(截圖,程式碼覆蓋率,…)
-
WebView的支援(很多App都用到了H5)
-
自定義控制元件的測試
-
是否需要原始碼
-
能否需要連著電腦
-
是否支援CI(持續整合)
-
….
我們首先來看看XCode內建的框架:XCTest。XCTest又可以分為兩部分:Unit Test 和 UI Test,分別對應單元測試和UI測試。有一些三方的測試庫也是基於XCTest框架的,這個在後文會講到。由於是Apple官方提供的,所以這個框架會不斷完善。
成熟的三方框架通常提供了很多封裝好的有好的介面,筆者綜合對比了一些,推薦以下框架:
單元測試:
以下三個框架都是BDD(Behavior-driven development) - 行為驅動開發。行為驅動開發簡單來說就是先定義行為,然後定義測試用例,接著再編寫程式碼。 實踐中發現,通常沒有那麼多時間來先定義行為,不過BDD中的domain-specific language (DSL)能夠很好的描述用例的行為。
UI測試
-
KIF 基於XCTest的測試框架,呼叫私有API來控制UI,測試用例用Objective C或Swift編寫。
-
appium 基於Client - Server的測試框架。App相當於一個Server,測試程式碼相當於Client,通過傳送JSON來操作APP,測試語言可以是任意的,支援android和iOS。
篇幅有限,本文會先介紹XCtest,接著三方的Unit框架會以Quick為例,UI Test框架側重分析KIF,appium僅僅做原理講解。
XCTest
對於XCTest來說,最後生成的是一個bundle。bundle是不能直接執行的,必須依賴於一個宿主程序。關於XCTest進行單元測試的基礎(XCode的使用,非同步測試,效能測試,程式碼覆蓋率等),我在這篇文章裡講解過,這裡不再詳細講解。
單元測試用例
比如,我有以下一個函式:
1 2 3 |
//驗證一段Text是否有效。(不能以空字元開頭,不能為空)
- (BOOL)validText:(NSString *)text error:(NSError *__autoreleasing *)error{
}
|
那麼,我該如何為這個函式編寫單元測試的程式碼?通常,需要考慮以下用例:
-
輸入以空白字元或者換行符開頭的,error不為空,返回 NO
-
輸入正確的內容,error為空,返回YES
-
輸入為nil,error不為空,返回 NO (邊界條件)
-
輸入為非NSString型別,驗證不通過,返回NO (錯誤輸入)
-
特殊輸入字元(標點符號,非英文等等)
UI測試
UI測試是模擬使用者操作,進而從業務處層面測試。關於XCTest的UI測試,建議看看WWDC 2015的這個視訊:
關於UI測試,有幾個核心類需要掌握
-
XCUIApplication 測試應用的代理
-
XCUIElement 一個UI上可見的檢視物件
-
XCUIElementQuery 查詢XCUIElement
UI測試還有一個核心功能是UI Recording。選中一個UI測試用例,然後點選圖中的小紅點既可以開始UI Recoding。你會發現:
隨著點選模擬器,自動合成了測試程式碼。(通常自動合成程式碼後,還需要手動的去調整)
在寫UI測試用例的時候要注意:測試行為而不是測試程式碼。比如,我們測試這樣一個case
進入Todo首頁,點選add,進入新增頁面,輸入文字,點選save。
測試效果如下:
對應測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (void)testAddNewItems{
//獲取app代理
XCUIApplication *app = [[XCUIApplication alloc] init];
//找到第一個tabeview,就是我們想要的tableview
XCUIElement * table = [app.tables elementBoundByIndex:0];
//記錄下來新增之前的數量
NSInteger oldCount = table.cells.count;
//點選Add
[app.navigationBars[@
"ToDo"
].buttons[@
"Add"
] tap];
//找到Textfield
XCUIElement *inputWhatYouWantTodoTextField = app.textFields[@
"Input what you want todo"
];
//點選Textfield
[inputWhatYouWantTodoTextField tap];
//輸入字元
[inputWhatYouWantTodoTextField typeText:@
"somethingtodo"
];
//點選儲存
[app.navigationBars[@
"Add"
].buttons[@
"Save"
] tap];
//獲取當前的數量
NSInteger newCount = table.cells.count;
//如果cells的數量加一,則認為測試成功
XCTAssert(newCount == oldCount + 1);
}
|
這裡是通過前後tableview的row數量來斷言成功或者失敗。
等待
通常,在檢視切換的時候有轉場動畫,我們需要等待動畫結束,然後才能繼續,否則query的時候很可能找不到我們想要的控制元件。
比如,如下程式碼等待VC轉場結束,當query只有一個table的時候,才繼續執行後續的程式碼。
1 2 3 4 5 |
[self expectationForPredicate:[NSPredicate predicateWithFormat:@
"self.count = 1"
]
evaluatedWithObject:app.tables
handler:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
//後續程式碼....
|
Tips: 當你的UI結構比較複雜的時候,比如各種巢狀childViewController,使用XCUIElementQuery的程式碼會很長,也不好維護。
另外,UI測試還會在每一步操作的時候截圖,方便對測試報告進行驗證。
檢視測試結果
使用基於XCTest的框架,可以在XCode的report navigator中檢視測試結果。
其中:
-
Tests 用來檢視詳細的測試過程
-
Coverage 用來檢視程式碼覆蓋率
-
Logs 用來檢視測試的日誌
點選圖中的紅色框指向的圖示可以看到每一步UI操作的截圖
除了利用XCode的GUI,還可以通過後文提到的命令列工具來測試,檢視結果。
Stub/Mock
首先解釋兩個術語:
-
mock 表示一個模擬物件
-
stub 追蹤方法的呼叫,在方法呼叫的時候返回指定的值。
通常,如果你採用純存的XCTest,推薦採用OCMock來實現mock和stub,單元測試的三方庫通常已集成了stub和mock。
那麼,如何使用mock呢?舉個官方的例子:
1 2 3 4 5 |
//mock一個NSUserDefaults物件
id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
//在呼叫stringForKey的時候,返回http://testurl
OCMStub([userDefaultsMock
stringForKey:@
"MyAppURLKey"
]).andReturn(@
"http://testurl"
);
|
再比如,我們要測試開啟其他App,那麼如何判斷確實打開了其他App呢?
1 2 3 |
id app = OCMClassMock([UIApplication class]);
OCMStub([app sharedInstance]).andReturn(app);
OCMVerify([app openURL:url]
|
使用Stub可以讓我們很方便的實現這個。
關於OCMock的使用,推薦看看objc.io的這篇文章
Quick
Quick是建立在XCTestSuite上的框架,使用XCTestSuite允許你動態建立測試用例。所以,使用Quick,你仍讓可以使用XCode的測試相關GUI和命令列工具。
使用Quick編寫的測試用例看起來是這樣子的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import Quick
import Nimble
class TableOfContentsSpec: QuickSpec {
override func spec() {
describe(
"the 'Documentation' directory"
) {
it(
"has everything you need to get started"
) {
let sections = Directory(
"Documentation"
).sections
expect(sections).to(contain(
"Organized Tests with Quick Examples and Example Groups"
))
expect(sections).to(contain(
"Installing Quick"
))
}
context(
"if it doesn't have what you're looking for"
) {
it(
"needs to be updated"
) {
let you = You(awesome:
true
)
expect{you.submittedAnIssue}.toEventually(beTruthy())
}
}
}
}
}
|
BDD的框架讓測試用例的目的更加明確,測試是否通過更加清晰。使用Quick,測試用例分為兩種:
單獨的用例 - 使用it來描述
it有兩個引數,
-
行為描述
-
行為的測試程式碼
比如,以下測試Dolphin行為,它具有行為is friendly和is smart
1 2 3 4 5 6 7 8 9 10 11 12 |
//Swift程式碼
class DolphinSpec: QuickSpec {
override func spec() {
it(
"is friendly"
) {
expect(Dolphin().isFriendly).to(beTruthy())
}
it(
"is smart"
) {
expect(Dolphin().isSmart).to(beTruthy())
}
}
}
|
可以看到,BDD的核心是行為。也就是說,需要關注的是一個類提供哪些行為。
用例集合,用describe和context描述
比如,驗證dolphin的click行為的時候,我們需要兩個用例。一個是is loud,一個是has a high frequency,就可以用describe將用例組織起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class DolphinSpec: QuickSpec {
override func spec() {
describe(
"a dolphin"
) {
describe(
"its click"
) {
it(
"is loud"
) {
let click = Dolphin().click()
expect(click.isLoud).to(beTruthy())
}
it(
"has a high frequency"
) {
let click = Dolphin().click()
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
|
context可以指定用例的條件:
比如
1 2 3 4 5 6 7 |
describe(
"its click"
) {
context(
"when the dolphin is not near anything interesting"
) {
it(
"is only emitted once"
) {
expect(dolphin!.click().count).to(equal(1))
}
}
}
|
除了這些之外,Quick也支援一些切入點,進行測試前的配置:
-
beforeEach
-
afterEach
-
beforeAll
-
afterAll
-
beforeSuite
-
afterSuite
Nimble
由於Quick是基於XCTest,開發者當然可以收使用斷言來定義測試用例成功或者失敗。Quick提供了一個更有好的Framework來進行這種斷言:Nimble
比如,一個常見的XCTest斷言如下:
1 |
XCTAssertTrue(ConditionCode,
"FailReason"
)11
|
在出錯的時候,會提示
1 |
XCAssertTrue failed, balabala
|
這時候,開發者要打個斷點,檢視下上下文,看看具體失敗的原因在哪。
使用Nimble後,斷言變成類似
1 2 3 4 |
expect(1 + 1).to(equal(2))
expect(3) > 2
expect(
"seahorse"
).to(contain(
"sea"
))
expect([
"Atlantic"
,
"Pacific"
]).toNot(contain(
"Mississippi"
))
|
並且,出錯的時候,提示資訊會帶著上下文的值資訊,讓開發者更容易的找到錯誤。
讓你的程式碼更容易單元測試
測試的準確性和工作量很大程度上依賴於開發人員的程式碼質量。
通常,為了單元測試的準確性,我們在寫函式(方法)的時候會借鑑一些函數語言程式設計的思想。其中最重要的一個思想就是
-
pure function(純函式)
何為Pure function?就是如果一個函式的輸入一樣,那麼輸出一定一樣。
比如,這樣的一個函式就不是pure function。因為它依賴於外部變數value的值。
1 2 3 4 5 6 |
static NSInteger value = 0;
- (NSInteger)function_1{
value = value + 1;
return
value;
}
|
而這個函式就是pure function,因為給定輸入,輸出一定一致。
1 2 3 4 |
- (NSInteger)function_2:(NSInteger)base{
NSInteger value = base + 1;
return
value;
}
|
所以,如果你寫了一個沒有引數,或者沒有返回值的方法,那麼你要小心了,很可能這個方法很難測試。
關於MVC
在良好的MVC架構的App中,
-
View只做純粹的展示型工作,把使用者互動通過各種方式傳遞到外部
-
Model只做資料儲存類工作
-
Controller作為View和Model的樞紐,往往要和很多View和Model進行互動,也是自動化包括程式碼維護的痛點。
所以,對Controller瘦身是iOS架構中比較重要的一環,一些通用的技巧包括:
邏輯抽離:
-
網路請求獨立。可以每個網路請求以Command模式封裝成一個物件,不要直接在Controller呼叫AFNetworking。
-
資料儲存獨立。建立獨立的Store類,用來做資料持久化和快取。
-
共有資料服務化(協議)。比如登入狀態等等,通過服務去訪問,這樣服務提供者之需要處理服務的質量,服務使用者則信任服務提供者的結果。
Controller與View解耦合
-
建立ViewModel層,這樣Controller只需要和ViewModel進行互動。
-
建立UIView子類作為容器,將一些View放到容器後再把容器作為SubView新增到Controller裡
-
建立可複用的Layout層,不管是AutoLayout還是手動佈局。
Controller與Controller解耦合
-
建立頁面路由。每一個介面都抽象為一個URL,跳轉僅僅通過Intent或者URL跳轉,這樣兩個Controller完全獨立。
如果你的App用Swift開發,那麼面向協議程式設計和不可變的值型別會讓你的程式碼更容易測試。
當然,iOS組建化對自動化測試的幫助也很大,因為不管是基礎元件還是業務元件,都可以獨立測試。組建化又是一個很大的課題,這裡不深入講解了。
KIF
KIF的全稱是Keep it functional。它是一個建立在XCTest的UI測試框架,通過accessibility來定位具體的控制元件,再利用私有的API來操作UI。由於是建立在XCTest上的,所以你可以完美的藉助XCode的測試相關工具(包括命令列指令碼)。
> KIF是個人非常推薦的一個框架,簡單易用。
使用KIF框架強制要求你的程式碼支援accessibility。如果你之前沒接觸過,可以看看Apple的文件
簡單來說,accessibility能夠讓視覺障礙人士使用你的App。每一個控制元件都有一個描述AccessibilityLabel。在開啟VoiceOver的時候,點選控制元件就可以選中並且聽到對應的描述。
通常UIKit的控制元件是支援accessibility的,自定定義控制元件可以通過程式碼或者Storyboard上設定。
在Storyboard上設定:
-
上面的通過Runtime Attributes設定(KVC)
-
下面的通過GUI來設定
通過程式碼設定:
1 2 3 |
[alert setAccessibilityLabel:@
"Label"
];
[alert setAccessibilityValue:@
"Value"
];
[alert setAccessibilityTraits:UIAccessibilityTraitButton];
|
如果你有些Accessibility的經驗,那麼你肯定知道,像TableView的這種不應該支援VoiceOver的。我們可以用條件編譯來只對測試Target進行設定:
1 2 3 4 5 6 7 |
#ifdef DEBUG
[tableView setAccessibilityValue:@
"Main List Table"
];
#endif
#ifdef KIF_TARGET (這個值需要在build settings裡設定)
[tableView setAccessibilityValue:@
"Main List Table"
];
#endif
|
使用KIF主要有兩個核心類:
-
KIFTestCase XCTestCase的子類
-
KIFUITestActor 控制UI,常見的三種是:點選一個View,向一個View輸入內容,等待一個View的出現
我們用KIF來測試新增一個新的ToDo
1 2 3 4 5 6 7 |
- (void)testAddANewItem{
[tester tapViewWithAccessibilityLabel:@
"Add"
];
[tester enterText:@
"Create a test to do item"
intoViewWithAccessibilityLabel:@
"Input what you want todo"
];
[tester tapViewWithAccessibilityLabel:@
"Save"
];
[tester waitForTimeInterval:0.2];
[tester waitForViewWithAccessibilityLabel:@
"Create a test to do item"
];
}
|
命令列
自動化測試中,命令列工具可以facebook的開源專案:
這是一個基於xcodebuild命令的擴充套件,在iOS自動化測試和持續整合領域很有用,而且它支援-parallelize並行測試多個bundle,大大提高測試效率。
安裝XCTool,
1 |
brew install xctool11
|
使用
1 2 3 4 5 |
path/to/xctool.sh \
-workspace YourWorkspace.xcworkspace \
-scheme YourScheme \
-reporter plain:/path/to/plain-output.txt \
run-test
|
並且,xctool對於持續整合很有用,iOS常用的持續整合的server有兩個:
優化你的測試程式碼
準確的測試用例
通常,你的你的測試用例分為三部分:
-
配置測試的初始狀態
-
對要測試的目標執行程式碼
-
對測試結果進行斷言(成功 or 失敗)
測試程式碼結構
當測試用例多了,你會發現測試程式碼編寫和維護也是一個技術活。通常,我們會從幾個角度考慮:
-
不要測試私有方法(封裝是OOP的核心思想之一,不要為了測試破壞封裝)
-
對用例分組(功能,業務相似)
-
對單個用例保證測試獨立(不受之前測試的影響,不影響之後的測試),這也是測試是否準確的核心。
-
提取公共的程式碼和操作,減少copy/paste這類工作,測試用例是上層呼叫,只關心業務邏輯,不關心內部程式碼實現。
一個常見的測試程式碼組織如下:
appium
appium採用了Client Server的模式。對於App來說就是一個Server,基於WebDriver JSON wire protocol對實際的UI操作庫進行了封裝,並且暴露出RESTFUL的介面。然後測試程式碼通過HTTP請求的方式,來進行實際的測試。其中,實際驅動UI的框架根據系統版本有所不同:
-
< 9.3 採用UIAutomation
-
>= 9.3 XCUITest
原因也比較簡單:Apple在10.0之後,移除了UIAutomation的支援,只支援XCUIT