前言html
若是有測試大佬發現內容不對,歡迎指正,我會及時修改。android
大多數的iOS App(沒有持續集成)迭代流程是這樣的ios
也就是說,測試是發佈以前的最後一道關卡。若是bug不能在測試中發現,那麼bug就會抵達用戶,因此測試的完整性和可靠性十分重要。git
目前,大多數App還停留在人工測試階段,人工測試投入的成本最低,可以保證核心功能的使用,並且測試人員不須要會寫代碼。github
可是,在不少測試場景下,人工測試的效率過低,容易出錯。舉兩個常見的例子:web
一個App的核心功能,在每一次發佈版本前的測試一定會跑一遍全部的測試用例,無論對應的業務在當前版本有沒有變化(天知道開發在作業務A的時候,對業務B有沒有影響),若是此次測出新的bug,測試人員在下一次發版測試中,又不得不作這些重複的工做。編程
開發在寫API請求相關代碼的時候沒有作數據容錯,測試在人工測試的時候都是正常的數據,因此測試經過。上線了以後,後臺配置數據的時候出了點小問題,致使大面積崩潰,boom~。swift
而後,老闆就要過來找你了xcode
本文所講解的均是基於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
|
再好比,咱們要測試打開其餘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的支持,只支持XCUITest。
對比KIF,appium有它的優勢:
跨平臺,支持iOS,Android
測試代碼能夠由多種語言編寫,這對測試來講門檻更低
測試腳本獨立與源代碼和測試框架
固然,任何框架都有缺點:
自定義控件支持很差
WebView的支持很差
總結
因爲我不是專業的iOS測試,關於測試的一點看法以下:
單元測試仍是選擇BDD框架,畢竟可讀性高一些,推薦Quick(Swift),Kiwi(Objective C)
UI測試優先推薦KIF,若是須要兼顧安卓測試,或者測試人員對OC/Swift很陌生,能夠採用appium
參考資料
Testing with Xcode 官方文檔,關於XCTest以及XCode有詳細的講解
objc.io關於測試的資料對於官方文檔的補充
騰訊移動品質中心 鵝廠移動品質中心,有不少好文章,強力推薦。
基於 KIF 的 iOS UI 自動化測試和持續集成 美團點評技術團隊寫的一篇博客