iOS 單元測試&UI測試(1)

語言: CN / TW / HK

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選項,如下圖所示:

image.png

  在測試程式碼執行之前,先來丟擲幾個問題?

    1. 測試程式碼執行之前需不需要啟動APP
    1. 需不需要呼叫AppDelegate中的didFinishLaunchingWithOptions方法?

  為了驗證這兩個問題的確切答案,在測試工程打上如下的幾個斷點,如下圖所示:

image.png

image.png

  接著執行測試工程中TestAppDemoTests.m檔案中的testExample方法,如下圖所示:

image.png

  首先,可以看到,App在模擬器中被啟動了,並且程式執行到了main.m檔案中設定的斷點處,如下圖所示:

image.png

  過掉斷點,程式執行到了AppDelegate.m檔案中設定的斷點處,如下圖所示:

image.png

  過掉斷點,列印了在測試方法中輸出的日誌資訊,如下圖所示:

image.png

  其中:

  • 紅框1:中的時間表示的是測試方法執行的時間
  • 紅框2:表示的是測試方法的名字
  • 紅框3:表示測試通過並且耗時0.001秒。

  根據以上的執行結果,可以很清楚的看到程式執行測試方法的時候是會啟動APP並且會呼叫執行didFinishLaunchingWithOptions方法的,在一些大型專案中,通常會在didFinishLaunchingWithOptions方法中執行一些耗時的方法,那麼這樣就不能快速進行測試了,為了解決這個問題,可以選擇建立一個FakeAppDelegate,只要在測試的時候在main函式中返回這個FakeAppDelegate物件就可以了,程式碼如下所示:

``` //FakeAppDelegate.h檔案中程式碼

import

NS_ASSUME_NONNULL_BEGIN

@interface FakeAppDelegate : UIResponder @end

NS_ASSUME_NONNULL_END

//FakeAppDelegate.m檔案中程式碼

import "FakeAppDelegate.h"

@interface FakeAppDelegate ()

@end

@implementation FakeAppDelegate

  • (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {

    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 )sceneSessions { }

@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檔案中列印如下日誌資訊:

image.png

image.png

  command+r執行App,輸出日誌資訊如下圖所示:

image.png

  執行TestAppDemoTests.m檔案中的testExample方法,輸出日誌資訊如下圖所示:

image.png

3.1.2 使用環境變數進行判斷

  在工程Scheme下的Test中的Debug模式下新增環境變數IS_TESTING,如下圖所示:

image.png

image.png

  然後在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,輸出日誌資訊如下圖所示:

image.png

  執行TestAppDemoTests.m檔案中的testExample方法,輸出日誌資訊如下圖所示:

image.png

3.2 Swift工程執行流程優化

  • 建立一個簡單的Swift工程TestSwiftDemo,如下圖所示:

image.png

  • 會發現在Swift工程中並沒有main.swift檔案

image.png

  • 建立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.self, capacity: Int(CommandLine.argc))

_ = UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClsName) ```

  • Test中配置如下圖所示環境變數

image.png

  • 執行結果如下圖所示:

image.png

4. 單元&UI測試原理初探

  在使用command + u執行測試工程之後,按照如下圖所示的方式開啟編譯之後產生的應用程式,會發現會多出AutoTestingDemoUITests-Runner這個應用程式。

image.png

  而模擬器中也會安裝這個應用程式,如下圖所示:

image.png

  進入到AutoTestingDemo這個APP包中,檢視其Frameworks中的檔案,發現多了以下幾個檔案:

image.png

image.png

  也就是說,是不是在一個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檔案,如下圖所示:

image.png

  接著在這個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檔案,如下圖所示:

image.png

 &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 ```

  接著執行程式,然後點選螢幕,日誌輸出如下圖所示:

image.png

  但以上程式碼有個問題,當再次點選螢幕的時候,應用程式會直接奔潰,如下圖所示:

image.png

  如果想要多次執行測試用例,可以通過以下的方式進行呼叫

``` - (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event { //管理者 XCTestSuite,用來管理測試用例 XCTestSuite suite = [XCTestSuite testSuiteForTestCaseClass:AppTests.class]; //測試用例 AppTests testCase = [AppTests new];

[suite addTest:testCase];
//遍歷其中所有的測試用例,呼叫其所有的測試方法
for (XCTest *test in suite.tests) {
    [test runTest];
}

} ```

  執行程式,點選螢幕,控制檯輸出資訊如下圖所示:

image.png

  未完待續...