iOS 單元測試&UI測試(1)
highlight: an-old-hope theme: smartblue
學習重點
單元&UI測試的意義
單元&UI測試執行流程探究及優化
單元&UI測試原理初探
1. 單元&UI測試
1.1 什麼是單元測試
單元測試是檢查每個程式碼單元(例如類或函式)是否能產生預期的結果,單元測試是獨立執行的,不依賴於其他模組或元件。
1.2 什麼是UI測試
UI
測試是屬於端到端的測試,是從應用程式啟動到結束的測試過程,完全按照使用者與應用程式互動的方式來複制與應用程式的互動,比單元測試慢的多,執行起來也更消耗資源。
1.3 需要進行測試的內容
&emsp測試應涵蓋以下的內容:
-
核心功能:模型類和方法及其與控制器的互動
-
UI
工作流程 -
特殊的邊界條件
-
Bug處理
1.4 測試原則(FIRST)
-
Fast
:測試模組應該是快速高效的 -
Independent/Isolated
:測試模組應該是獨立、相互不影響的 -
Repeatable
:測試例項應該是可以重複使用的,測試結果應該是相同的 -
Self-validating
:測試應完全自動化。輸出結果要麼是“成功”,要麼是“失敗” -
Timely
:理想情況下,應該在編寫要測試的生產程式碼之前編寫測試(測試驅動開發)
2. 單元&UI測試執行流程探究
首先,使用Xcode
建立一個iOS
工程,勾選上Include Tests
選項,如下圖所示:
在測試程式碼執行之前,先來丟擲幾個問題?
-
- 測試程式碼執行之前需不需要啟動
APP
?
- 測試程式碼執行之前需不需要啟動
-
- 需不需要呼叫
AppDelegate
中的didFinishLaunchingWithOptions
方法?
- 需不需要呼叫
為了驗證這兩個問題的確切答案,在測試工程打上如下的幾個斷點,如下圖所示:
接著執行測試工程中TestAppDemoTests.m
檔案中的testExample
方法,如下圖所示:
首先,可以看到,App
在模擬器中被啟動了,並且程式執行到了main.m
檔案中設定的斷點處,如下圖所示:
過掉斷點,程式執行到了AppDelegate.m
檔案中設定的斷點處,如下圖所示:
過掉斷點,列印了在測試方法中輸出的日誌資訊,如下圖所示:
其中:
紅框1
:中的時間表示的是測試方法執行的時間紅框2
:表示的是測試方法的名字紅框3
:表示測試通過並且耗時0.001
秒。
根據以上的執行結果,可以很清楚的看到程式執行測試方法的時候是會啟動APP
並且會呼叫執行didFinishLaunchingWithOptions
方法的,在一些大型專案中,通常會在didFinishLaunchingWithOptions
方法中執行一些耗時的方法,那麼這樣就不能快速進行測試了,為了解決這個問題,可以選擇建立一個FakeAppDelegate
,只要在測試的時候在main
函式中返回這個FakeAppDelegate
物件就可以了,程式碼如下所示:
``` //FakeAppDelegate.h檔案中程式碼
import
NS_ASSUME_NONNULL_BEGIN
@interface FakeAppDelegate : UIResponder
NS_ASSUME_NONNULL_END
//FakeAppDelegate.m檔案中程式碼
import "FakeAppDelegate.h"
@interface FakeAppDelegate ()
@end
@implementation FakeAppDelegate
-
(BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary
return YES; }
pragma mark - UISceneSession lifecycle
-
(UISceneConfiguration )application:(UIApplication )application configurationForConnectingSceneSession:(UISceneSession )connectingSceneSession options:(UISceneConnectionOptions )options {
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; }
-
(void)application:(UIApplication )application didDiscardSceneSessions:(NSSet
@end
//main.m檔案中程式碼
import
import "AppDelegate.h"
import "FakeAppDelegate.h"
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. BOOL isShouldReturnFakeAppDelegate = NO;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
} ```
你也許會注意到isShouldReturnFakeAppDelegate
這個區域性變數的值為NO
,這樣的話不就沒什麼作用了嗎?這也正是我想丟擲的問題,應該如何設定isShouldReturnFakeAppDelegate
的值呢?在執行的過程中,如何知道此時是測試方法的呼叫還是實際APP
的執行呢?讀者不妨來思考一下這個問題,我會在接下來的討論中分享兩種解決方案。
3. 單元&UI測試執行流程優化
3.1 OC工程執行流程優化
3.1.1 使用runtime API
進行判斷
首先你應該注意到的是測試工程中類的繼承順序為:TestAppDemoTests
-->XCTestCase
-->XCTest
,那麼可以從這裡入手,判斷XCTest
這個類是否存在就可以了,在main.m
檔案中編寫如下的程式碼:
```
import
import "AppDelegate.h"
import "FakeAppDelegate.h"
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. BOOL isShouldReturnFakeAppDelegate = NSClassFromString(@"XCTest") != nil;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
} ```
然後分別在AppDelegate.m
檔案以及FakeAppDelegate.m
檔案中列印如下日誌資訊:
command+r
執行App
,輸出日誌資訊如下圖所示:
執行TestAppDemoTests.m
檔案中的testExample
方法,輸出日誌資訊如下圖所示:
3.1.2 使用環境變數進行判斷
在工程Scheme
下的Test
中的Debug
模式下新增環境變數IS_TESTING
,如下圖所示:
然後在main.m
檔案中編寫如下程式碼,獲取環境變數IS_TESTING
的值
```
import
import "AppDelegate.h"
import "FakeAppDelegate.h"
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. BOOL isShouldReturnFakeAppDelegate = [[NSProcessInfo processInfo].environment[@"IS_TESTING"] boolValue];;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
} ```
command+r
執行App
,輸出日誌資訊如下圖所示:
執行TestAppDemoTests.m
檔案中的testExample
方法,輸出日誌資訊如下圖所示:
3.2 Swift
工程執行流程優化
- 建立一個簡單的
Swift
工程TestSwiftDemo
,如下圖所示:
- 會發現在
Swift
工程中並沒有main.swift
檔案
- 建立
main.swift
檔案並在這個檔案中編寫如下程式碼:
``` import UIKit
var appDelegateClsName = NSStringFromClass(AppDelegate.self)
//兩種判斷方式任選其一
//1.根據XCTest是否存在判斷是否正在執行測試方法 if NSClassFromString("XCTest") != nil {
appDelegateClsName = NSStringFromClass(FakeAppDelegate.self)
}
//2.根據環境變數判斷是否正在執行測試方法 if ProcessInfo.processInfo.environment["IS_TESTING"] == "true" { appDelegateClsName = NSStringFromClass(FakeAppDelegate.self) }
let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer
_ = UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClsName) ```
- 在
Test
中配置如下圖所示環境變數
- 執行結果如下圖所示:
4. 單元&UI測試原理初探
在使用command + u
執行測試工程之後,按照如下圖所示的方式開啟編譯之後產生的應用程式,會發現會多出AutoTestingDemoUITests-Runner
這個應用程式。
而模擬器中也會安裝這個應用程式,如下圖所示:
進入到AutoTestingDemo
這個APP
包中,檢視其Frameworks
中的檔案,發現多了以下幾個檔案:
也就是說,是不是在一個ipa
包中只要包含了這幾個動態庫,就可以在專案中執行測試程式碼了呢?而XCTest.framework
又是從哪裡拷貝進來的呢?其實是從Xcode
中的以下路徑中獲取的:
-
XCTest.framework
(模擬器裝置):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework
-
XCTest.framework
(真機裝置):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework
只要在工程中添加了XCTest.framework
,就可以不建立測試target
也能在工程中進行程式碼測試了,首先建立一個xcconfig
檔案,如下圖所示:
接著在這個xcconfig
檔案中按照如下的方式進行配置:
``` //1.設定動態庫標頭檔案路徑 HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/Headers"
//2.連結動態庫
//2.1傳統方式 OTHER_LDFLAGS = $(inherited) -F "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" -framework "XCTest"
//2.2
//3.配置rpath 解決 崩潰Reason: image not found LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" ```
然後主工程引用這個xcconfig
檔案,如下圖所示:
&emsp建立一個AppTests類,其程式碼如下所示:
``` //AppTests.h檔案中的程式碼
import
NS_ASSUME_NONNULL_BEGIN
@interface AppTests : XCTestCase
-
(void)testExample1;
-
(void)testExample2;
@end
NS_ASSUME_NONNULL_END
//AppTests.m檔案中的程式碼
import "AppTests.h"
@implementation AppTests
-
(void)testExample1 { NSLog(@"-------testExample1-------"); }
-
(void)testExample2 { NSLog(@"-------testExample2-------"); }
@end ```
然後在ViewController.m檔案中編寫如下所示的程式碼:
```
import "ViewController.h"
import
import "AppTests.h"
@interface ViewController ()
@end
@implementation ViewController
-
(void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. }
-
(void)touchesBegan:(NSSet
)touches withEvent:(UIEvent )event { //管理者 XCTestSuite,用來管理測試用例 XCTestSuite suite = [XCTestSuite defaultTestSuite]; //測試用例 AppTests testCase = [AppTests testCaseWithSelector:@selector(testExample1)]; [suite addTest:testCase]; //遍歷其中所有的測試用例,呼叫其所有的測試方法 for (XCTest *test in suite.tests) { [test runTest]; }
}
@end ```
接著執行程式,然後點選螢幕,日誌輸出如下圖所示:
但以上程式碼有個問題,當再次點選螢幕的時候,應用程式會直接奔潰,如下圖所示:
如果想要多次執行測試用例,可以通過以下的方式進行呼叫
```
- (void)touchesBegan:(NSSet
[suite addTest:testCase];
//遍歷其中所有的測試用例,呼叫其所有的測試方法
for (XCTest *test in suite.tests) {
[test runTest];
}
} ```
執行程式,點選螢幕,控制檯輸出資訊如下圖所示:
未完待續...