KIF-- iOS UI 自動化測試探索
在我們探索自動化測試之前,我們先了解一下自動化測試的優缺點和還有,什么樣的業務適合自動化測試。
自動化測試
自動化測試就是寫一些測試代碼,用代碼代替人工去完成模塊和業務的測試。
其實不管是開發還是測試,如果你在不斷的做重復性工作的時候,就應該問自己一個問題:是不是有更高效的辦法?
-
自動化測試的優點:
- 測試速度快,避免重復性工作
- 避免regression(回退),讓開發更有信心去修改,優化甚至重構代碼。(有測試代碼做依托,不怕業務邏輯丟失,混亂)
- 測試結果一致性
- 自動化測試的實現,有助于持續集成的可行性和可靠性提升
- 強化開發人員編寫高質量代碼(自動化測試,不通過,不能提交合并)
-
自動化測試的一些缺點:
- 開發和維護成本高,需要專業的測試人員
- 不能完全替代人工測試
- 本身的測試代碼的準確性,還有無法保證測試的準確性-讓代碼去判斷一段邏輯是否正確還是可行的,但是要判斷一個控件是否顯示正確,代碼很難實現
- 團隊的建設和方案的選取等一系列的問題
所以,在做自動化測試之前,我們首先要針對項目提出幾個問題:
- 這個測試業務的變動是否頻繁
- 這個測試業務是否屬于核心功能
- 編寫測試代碼的成本是多少,是否劃算
- 自動化測試能保證測試結果的準確么
通常我們只會選擇那些業務穩定,需要頻繁測試的部分來編寫自動化腳本,其余的依然得人工測試,人工測試是 iOS App 開發中不可缺少的一部分。
測試種類
從是否接觸源代碼的角度來分類: 測試分為黑盒和白盒。
白盒測試的時候,測試人員是可以直接接觸待測試App的源代碼的。白盒測試更多的是單元測試,測試人員針對各個單元進行各種可能的輸入分析,然后測試其輸出。白盒測試的測試代碼通常由iOS開發編寫。
黑盒測試。黑盒測試的時候,測試人員不需要接觸源代碼。是從App層面對其行為以及UI的正確性進行驗證,黑盒測試由iOS測試完成。
從業務層次來說 iOS 測試通常只有以下兩個層次:
Unit,單元測試,保證每一個類都能夠正常工作
UI,UI 測試,也叫做集成測試,從業務層的角度保證各個業務可以正常工作。
框架選擇
測試框架五花八門,一定要選擇適合自己團隊的,測試效率,集成難易度,維護難易度等等,選擇框架的時候我們要考慮一下幾個方面:
- 測試代碼編寫的成本
- 是否可調式,調試是否便利
- 測試框架本身的穩定性
- 測試報告是否詳細(截圖,代碼覆蓋率...)
- WebView 的支持(H5混合的 App)
- 自定義控件的測試支持
- 是否需要源代碼
- 是否需要連著電腦和設備
- 是否支持持續集成
其中單元測試,我們上一篇著重介紹了 BDD 的老牌測試框架 Kiwi ,就不多說了。
UI測試,UI 測試的框架有很多,有的是以 UI Automation 為基礎,對其進行補充和優化,包括擴展型 UI Automation 和驅動型 UI Automation。
還有一些框架類型是私有 API 和注入編譯型等。
在以上分類中挑選具有代表性的自動化框架:UI Automation、Appium、KIF、Frank、UI Testing 進行對比,下表是這幾種測試框架的特點對比:
結合上面我們選擇框架要考慮的幾個方面,KIF框架已經展現了它的優勢,并且KIF使用XCTest框架,使得其測試流程iOS程序的單測無異,可完全復用單測的持續集成流程,維護持續集成的成本相對降低;另外,KIF是一個活躍的開源測試框架,可擴展性好,升級更新快,有活躍社區來探討和解決使用過程中遇到的問題。就是今天我們要介紹的重點,KIF。
KIF
KIF的全稱是Keep it functional
。利用 Apple 給所有控件提供的輔助屬性 accessibility attributes來定位和獲取元素,完成界面的交互操作;結合使用 Xcode 的 XCTest 測試框架,擁有 XCTest 測試框架的特性,使得測試用例能以 command line build 工具運行并獲取測試報告。
KIF 搭建
我們首先應該在工程項目中創建基于 Cocoa Touch Testing Bundle 模板的 Target ,并確保創建的 Target 的屬性有如下設置:
“Build Phases”:設置Target Dependencies,UI自動化測試固然要依賴應用程序的App產物,所以需保證應用程序 Target 被添加在 Test Target 的 Target Dependencies 中。
“Build Settings”:設置 “Bundle loader”為:$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp;MyApp使你自己項目的路徑
設置 “Test Host” 為:$(BUILT_PRODUCTS_DIR);
設置 “Wrapper Extensions” 為:xctest。
- cocoaPods導入:
target 'Your Apps' do
...
end
target 'Acceptance Tests' do
pod 'KIF', :configurations => ['Debug']
end
-
手動導入,最新的
framework
方式導入,非常頭疼- 下載KIF源碼,選擇
KIFFramework
這個scheme
編譯,products 里面生成KIF.framework
,show in finder 把它拷貝到我們需要測試的項目里面去 - 打開 Xcode file new
Add target
選擇 iOS Unit Testing Bundle 或者 iOS UI Testing bundle 設置一個自己喜歡的target 名稱 - 選擇我們新建的 target 點擊
Build Phases
下的Link Binary With Libaries
,添加我們剛拷貝過來的KIF.framework
,系統依賴庫QuartzCore.framework
、CoreGraphics.framework
- 然后選擇這個 target 的
Build Settings
,在Other Linker Flags
里面添加-framework IOKit
和-ObjC
這兩選項 - 接著設置
User Header Search Paths
和Framework Search Paths
的路徑為我們新建的 target - 最后,設置
Bundle Loader
為"$(BUILT_PRODUCTS_DIR)/MyApplication.app/MyApplication
" 里面的MyApplication
是自己自己項目的名字 - 最后一步,最重要的一部,先把項目跑一遍,生成
MyApplication.app
之后再執行command + U
開始 testing
- 下載KIF源碼,選擇
-
現在可以開始寫我們的測試用例了
開始之前,我想來張圖,KIF 基于蘋果給所有控件添加的一個accessibility 屬性來實現的,所以在 Storyboard 上我們有兩種方式設置
還可以通過代碼設置:
[alert setAccessibilityLabel:@"Label"];
[alert setAccessibilityValue:@"Value"];
[alert setAccessibilityTraits:UIAccessibilityTraitButton];
為了跟原業務代碼隔離,我們在業務代碼中應該建立宏來隔離我們設置 accessibility 屬性的代碼,如下面的例子:
#ifdef DEBUG
[tableView setAccessibilityValue:@"Main List Table"];
#endif
#ifdef KIF_TARGET (這個值需要在build settings里設置)
[tableView setAccessibilityValue:@"Main List Table"];
#endif
測試用例的編寫和組織
- accessibility屬性設置
accessibility 屬性是Apple給視覺障礙人群提供完全無障礙使用的基本屬性,該屬性表明了UI元素的可訪問性、是什么、做什么以及會觸發什么樣的操作。原生的UIKit控件默認提供了這些信息,然而,自定義的控件則需要對該屬性進行設置,設置方式可參考下面幾點:
- 設置方式:storyboard 設置,代碼設置
- 查看方式:Xcode打開
Open Developer Tool
開啟模擬器的 Accessibility Inspector功能,即可看到控件的 accessibility 屬性。 - 設置建議:設置的 AccessibilityLabel 屬性值要有實際意義(用戶可理解)因為設置這個屬性后用戶可以通過 VoiceOver訪問;用戶不可訪問的控件,比如某些放置控件的容器等應該設置為 AccessibilityIdentifier 。
- KIF 常用操作接口(
KIFUITestActor.h
里可查閱)
tapThisView:- (void)tapViewWithAccessibilityLabel:(NSString *)label;
waitForView:- (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label;
//注意:函數返回了對應View的指針,可以對返回值取數據,從而進行一些判斷
enterTextIntoView: - (void)enterText:(NSString *)text intoViewWithAccessibilityLabel:(NSString *)label;
tapRowOnTableView:- (void)tapRowAtIndexPath:(NSIndexPath *)indexPath inTableViewWithAccessibilityIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
dismisses a system alert: - (void)acknowledgeSystemAlert;
擴展:我們還可以對 KIFUITestActor 類進行擴展,利用 KIFUITestActor 中的私有函數,使 AccessibilityIdentifier 代替 Label 識別元素,完成 tapThisView 、waitForView 等操作。
- KIF測試用例集操作(KIFTestCase.h 中可查閱)
/*!
* @abstract This method runs once before executing the first test in the class.
* @discussion This should be used for navigating to the starting point in the app where all tests will start from. Because this method is not guaranteed to run in the same instance as tests, it should not be used for setting up instance variables but can be used for setting up static variables.
*/
/*
在本類中第一個 test case 執行前執行一次,用來執行本類中各個測試函數的公共操作
//注意:因為不能保證這個方法與 test case 是同一個類實例,所以不能用來設置實例變量的值,但是可以設置靜態變量
*/
- (void)beforeAll;
/*!
* @abstract This method runs before each test.
* @discussion This should be used for any common tasks required before each test. Because this method is guaranteed to run in the same instance as tests, it can be used for setting up instance variables.
*/
//在每個具體 test case 執行前執行一次,用來執行各個函數需要的測試環境
//注意因為確保這個方法與 test case 是同一個類實例,可以用來設置實例變量
- (void)beforeEach;
/*!
* @abstract This method runs after each test.
* @discussion This should be used for restoring the app to the state it was in before the test. This could include conditional logic to recover from failed tests.
*/
//在每個具體 test case 執行完之后執行一次,用來清除狀態,恢復至 test 之前的狀態,可以包含一些條件判斷邏輯,從失敗的 test case 中恢復,以確保不影響之后的測試
- (void)afterEach;
/*!
* @abstract This method runs once after executing the last test in the class.
* @discussion This should be used for navigating back to the initial state of the app, where it was before @c beforeAll. This should also be used for tearing down any static methods created by @c beforeAll.
*/
//執行完本類的最后一個 test case 之后執行一次,用于將 App 恢復至測試的初始狀
- (void)afterAll;
/*!
* @discussion When @c YES, rather than failing the test and advancing on the first failure, KIF will stop executing tests and begin spinning the run loop. This provides an opportunity for inspecting the state of the app when the failure occurred.
*/
- 系統的功能實現(
KIFSystemTestActor.h
中可查閱)
模擬用戶旋轉設備:- (void)simulateDeviceRotationToOrientation:(UIDeviceOrientation)orientation;
對當前屏幕截圖并存儲到硬盤中:- (void)captureScreenshotWithDescription:(NSString *)description;
用例組織
-
設計單個測試用例
- a.設置測試所需要的環境
- b.測試用例的具體測試邏輯
- c.恢復 App 至此次測試前的狀態
a,c可用beforeEach
和alterEach
來實現,這樣保證了每個用例之間的獨立性和穩定性
一般來說,可將用例按功能分成若干個用例集,每個用例集按校驗點或者功能點分成若干個用例,這樣方便測試用例的管理和維護。某些含有耗費時間多,耗費資源多的公共操作的用例可以集合成一個用例集,在用例集運行前統一執行。
-
設計用例集
- 1.設置用例集需要的環境,公共操作
- 2.設計各個用例
- 3.恢復 App 至用例集測試的初始狀態
1和3 步驟可以用beforeAll
和afterAll
來實現。下面簡單展示一個用例集的書寫:
#import "CrazyTests.h"
#import <KIF/KIFUITestActor-IdentifierTests.h>
#import <KIF/KIFUITestActor-ConditionalTests.h>
@implementation CrazyTests
- (void)beforeAll
{
[self setTestModel];
}
- (void)afterAll
{
[self resetTestModel];
[self cleanHistory];
}
- (void)beforeEach
{
[self setTestModel];
}
- (void)afterEach
{
[self cleanParams];
}
- (void)testNameTask
{
[tester enterText:_pp.nickName intoViewWithAccessibilityLabel:@"name"];
[tester enterText:_pp.realName intoViewWithAccessibilityLabel:@"password"];
[tester tapViewWithAccessibilityLabel:@"login"];
}
#pragma mark-- setting
- (void)setTestModel
{
_pp = [Person new];
_pp.age = 20;
_pp.nickName = @"crazy";
_pp.realName = @"hey";
_pp.cardId = @"123456789";
}
- (void)resetTestModel
{
_pp = nil;
}
- (void)cleanHistory
{
_pp = nil;
}
- (void)cleanParams
{
_pp = nil;
}
上述例子,只是簡單說明。我們書寫用例集應該遵循如下規則:
- 將頁面上對元素的發現,操作處理抽象為相應的類,返回操作結果
- 封裝盡可能多的工具類
- 測試用例只關注用例邏輯,步驟盡量簡潔
我們可以利用 KIF 的私有 api 封裝我們的工具類。
用例的獨立運行和 retry 機制
失敗用例是不可避免的,上述用例的組織方式,降低了用例間的依賴性,但是并不能完全消除失敗用例對后續用例執行的影響。如果能讓每個用例獨立啟動 App 執行 case,則能保證后面執行用例不受執行失敗用例的影響。如果在 case 運行失敗后,還可以進行 retry 重試,能提高用例運行的穩定性。xctool這個工具能給我們帶來這樣的功能,我們用 xctool 命令先 build-tests 構建 App,然后循環啟動 App 來 run-tests用例,用例失敗后,重新執行。下面是是一個 xctool 獨立運行用例的簡單示例:
xctool build-tests -workspace myApp.xcworkspace -scheme myKIFTestScheme -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus'
array=( TimerTests HistoryTests )
for data in ${array[@]}
do
xctool -reporter pretty -reporter junit:tmp/test-report-tmp.xml -workspace myApp.xcworkspace -scheme myKIFTestScheme run-tests -only myKIFTestTarget:${data} -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus'
done
一般我們測試,團隊大了或者分工特別細的話,就需要接入自動化持續集成。后續大家可以自行了解,主要框架有Jenkins Fastlane等。
優化測試用例
當測試用例寫多了,我們也會重構我們的測試用例代碼。通常,我們應該從幾個角度去考慮:
- 不要測試私有方法(封裝是 OOP 的核心思想之一,不要為了測試破壞封裝)
- 對測試用例分組(功能,業務相似)
- 對單個用例保證測試獨立(不受之前測試的影響,不影響之后的測試),這是測試是否準確的核心
- 提取公共的代碼和操作,減少 copy/paste這樣的工作,測試用例是上層調用,只關心業務邏輯,不關心內部代碼實現
總結
KIF 因為是建立在 XCTest
框架之上的,所以非常適合我們開發者上手,而且利用私有 API,我們可以很方便的測試 UI 層面和單元測試等
參考: