Building stable testing by
isolating network layer
Mark Chang - iOS Developer
shea4a71@gmail.com
Test Corner #16 @ Cardinal Blue (PicCollage)
Motivation
• 😭 執⾏行行⾃自動化測試,最後發現是網路路問題造成錯誤
• 😞 測試因為執⾏行行環境不同⽽而造成失敗
• 🙄 還沒測到要測試的功能,測試就 GG 了了
• 😩 不同時間,執⾏行行結果不同...
非營業時間 營業時間
提升測試穩定度 ☀
加速測試執行速度 🚀
Outline
• UI Test
• Introduction to “WireMock" mock server tool
• How to redirect API host
• Unit Test
• Stub and Mock (using OCMock library)
• Implementing some test cases
Loading Finish Error
UI Test
要怎麼安全地改動程式後,
讓 App 容易易被測試?
Solution
• 建立⼀一個本機端的 Mock Server,回傳罐頭資訊
• 測試資料可⾃自⾏行行掌握
• App 端只需修改連線的 API 串串接到 Mock Server
• 所需的異異動較⼩小
• 降低 API Response 的等待時間,及網路路的不確定性
WireMock
• ⽤用來來模擬 API 回應資料的⼯工具 (Download)
• 執⾏行行在本機端
java -jar wiremock-standalone-2.17.0.jar --port 1234
WireMock
• 執⾏行行指令後,產出 __file 跟 mappings 資料夾
• __file: API response 的 JSON 檔案
• mappings: 設定 URL 與 JSON 檔案的對應關係
• 詳細可參參考此 Link 中的 Mock Server Configuration
Redirect URL to Mock Server
• 將 Production API URL 轉導⾄至 Mock Server API URL
• ⽅方法 1 : Charles Proxy (Map Remote)
• ⽅方法 2 : Xcode Configuration (.xcconfig)
https://jsonplaceholder.typicode.com/todos
http://localhost:1234/todos
Production
Mock Server
Charles Proxy
Production
Mock Server
Xcode Configuration
• 透過 Build Configuration 來來切換環境
• 詳細設定步驟可參參考此 Link
Xcode Configuration
- (NSString *)apiHost {
return [[NSBundle mainBundle].infoDictionary[@"API HOST"]
stringByReplacingOccurrencesOfString:@""
withString:@""];
}
App 裡呼叫 API class 的 API HOST 位址,讀取設定檔裡的位址
API_HOST = https://jsonplaceholder.typicode.com
API_HOST = http://localhost:1234
API_HOST = https://jsonplaceholder.typicode.com
Real Data Mock Data
Demo
Unit Test
How do you testing without making a real request ?
Stub and Mock
Stub and Mock
• Stub
• Fakes a response to method calls of an object
class StubTimeMachineAPI: TimeMachineAPI {
var videoUrl = "https://www.youtube.com/watch?v=SQ8aRKG9660"
func getVideoFor(year: Int) -> String {
return videoUrl
}
}
Reference
Stub and Mock
• Mock
• Let you check if a method call is performed or if a
property is set
class MockTimeMachine: TimeMachine {
var timeTravelWasCalled = false
func timeTravelTo(year: Int) {
timeTravelWasCalled = true
}
}
Reference
Stub never fail a unit test but a mock can
What's the difference
between a stub and mock?
Reference
Real World
• 可能需要改程式架構,讓可測試性提⾼高
• Legacy code 可能改不動,或是改完產⽣生更更⾼高的風險
• 想到⼀一堆前置動作就放棄惹 😐
Solution
• 整合測試的 Unit Test
• 測試的單位為⼀一個畫⾯面 (ViewController)
• 有點把 Unit Test 當 UI Test ⽤用 😆
• 快速提升 Code Coverage,CP 值頗⾼高
• 先求有,再求好
OCMock Library
• Class Mock
• Partial Mock
• Stubbing methods that return objects
id classMock = OCMClassMock([SomeClass class]);
id partialMock = OCMPartialMock(anObject);
OCMStub([mock someMethod]).andReturn(anObject);
Test Case: 驗證資料載入完成
/**
驗證 Cell 顯⽰示的資料
*/
- (void)testCellData {
// Arrange - 讀取本機 JSON file,模擬 API 回傳資料
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
// Assert - 驗證 Cell 顯⽰示資料
UITableView *todoTableView = todoViewController.tableView;
[responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL *
_Nonnull stop) {
UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]];
XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]);
UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ?
UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
XCTAssertEqual(cell.accessoryType, accessoryType);
}];
}
/**
驗證 Cell 顯⽰示的資料
*/
- (void)testCellData {
// Arrange - 讀取本機 JSON file,模擬 API 回傳資料
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
// Assert - 驗證 Cell 顯⽰示資料
UITableView *todoTableView = todoViewController.tableView;
[responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL *
_Nonnull stop) {
UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]];
XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]);
UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ?
UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
XCTAssertEqual(cell.accessoryType, accessoryType);
}];
}
Stub API 回傳的資料
直接讀取本機端 API 回傳的 JSON 資料 ( todos.json )
/**
驗證 Cell 顯⽰示的資料
*/
- (void)testCellData {
// Arrange - 讀取本機 JSON file,模擬 API 回傳資料
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
// Assert - 驗證 Cell 顯⽰示資料
UITableView *todoTableView = todoViewController.tableView;
[responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL *
_Nonnull stop) {
UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]];
XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]);
UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ?
UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
XCTAssertEqual(cell.accessoryType, accessoryType);
}];
}
初始化畫⾯面
/**
驗證 Cell 顯⽰示的資料
*/
- (void)testCellData {
// Arrange - 讀取本機 JSON file,模擬 API 回傳資料
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
// Assert - 驗證 Cell 顯⽰示資料
UITableView *todoTableView = todoViewController.tableView;
[responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL *
_Nonnull stop) {
UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]];
XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]);
UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ?
UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
XCTAssertEqual(cell.accessoryType, accessoryType);
}];
}
驗證 Cell 旁的 checkmark驗證 Cell 上的字串串
Refactor
• 有了了初步的 Unit Test 就可以來來重構程式
• 把 MVC (Model-View-Controller) 架構改⽤用 MVVM
(Model-View-ViewModel) 架構
- (void)testFetchDataSuccessful {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[mockTodoViewModel setExpectationOrderMatters:YES];
// 以下狀狀態不會被執⾏行行
OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]);
// 以下狀狀態會依序被執⾏行行
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]);
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]);
[mockTodoViewModel fetchData];
OCMVerifyAll(mockTodoViewModel);
XCTAssertEqual([mockTodoViewModel numberOfCells], 5);
MKCTodoCellViewModel *firstTodoCellViewModel =
[mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1");
XCTAssertFalse(firstTodoCellViewModel.completed);
}
- (void)testFetchDataSuccessful {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[mockTodoViewModel setExpectationOrderMatters:YES];
// 以下狀狀態不會被執⾏行行
OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]);
// 以下狀狀態會依序被執⾏行行
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]);
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]);
[mockTodoViewModel fetchData];
OCMVerifyAll(mockTodoViewModel);
XCTAssertEqual([mockTodoViewModel numberOfCells], 5);
MKCTodoCellViewModel *firstTodoCellViewModel =
[mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1");
XCTAssertFalse(firstTodoCellViewModel.completed);
}
Stub API 回傳的資料
直接讀取本機端 API 回傳的 JSON 資料 ( todos.json )
- (void)testFetchDataSuccessful {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[mockTodoViewModel setExpectationOrderMatters:YES];
// 以下狀狀態不會被執⾏行行
OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]);
// 以下狀狀態會依序被執⾏行行
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]);
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]);
[mockTodoViewModel fetchData];
OCMVerifyAll(mockTodoViewModel);
XCTAssertEqual([mockTodoViewModel numberOfCells], 5);
MKCTodoCellViewModel *firstTodoCellViewModel =
[mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1");
XCTAssertFalse(firstTodoCellViewModel.completed);
}
先 Loading 再 Finish
驗證 function 執⾏行行順序
不會進入 Error 狀狀態
呼叫 API
- (void)testFetchDataSuccessful {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0
error:nil];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil])
failureHandler:OCMOCK_ANY]);
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[mockTodoViewModel setExpectationOrderMatters:YES];
// 以下狀狀態不會被執⾏行行
OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]);
// 以下狀狀態會依序被執⾏行行
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]);
OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]);
[mockTodoViewModel fetchData];
OCMVerifyAll(mockTodoViewModel);
XCTAssertEqual([mockTodoViewModel numberOfCells], 5);
MKCTodoCellViewModel *firstTodoCellViewModel =
[mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1");
XCTAssertFalse(firstTodoCellViewModel.completed);
}
驗證資料量量正確,及 Table View 上第⼀一個 Cell 顯⽰示的資料符合預期
如何測試?
• API 回傳不合法資料
• 網路路連線異異常
• 畫⾯面載入中
• 驗證 UI 反應⾏行行為
Error conditions and exceptions are
nearly impossible to test without mocks
that can simulate those errors.
Test Case: 驗證 API 回傳不合法資料
#pragma mark - API 回傳異異常資料
- (void)testApiResponseInvalidData {
// Arrange
id invalidResponseObject = @[@"a", @"b", @"c"];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY,
invalidResponseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act
MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init];
[todoViewModel fetchData];
// Assert
XCTAssertEqual(todoViewModel.currentUiState, UIStateError);
}
#pragma mark - API 回傳異異常資料
- (void)testApiResponseInvalidData {
// Arrange
id invalidResponseObject = @[@"a", @"b", @"c"];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY,
invalidResponseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act
MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init];
[todoViewModel fetchData];
// Assert
XCTAssertEqual(todoViewModel.currentUiState, UIStateError);
}
Stub API 回應不合法資料 (如: 不符的資料格式)
#pragma mark - API 回傳異異常資料
- (void)testApiResponseInvalidData {
// Arrange
id invalidResponseObject = @[@"a", @"b", @"c"];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY,
invalidResponseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act
MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init];
[todoViewModel fetchData];
// Assert
XCTAssertEqual(todoViewModel.currentUiState, UIStateError);
}
呼叫 API
#pragma mark - API 回傳異異常資料
- (void)testApiResponseInvalidData {
// Arrange
id invalidResponseObject = @[@"a", @"b", @"c"];
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
OCMStub([mockApiService fetchTodoListWithSuccessHandler:
([OCMArg invokeBlockWithArgs:OCMOCK_ANY,
invalidResponseObject, nil])
failureHandler:OCMOCK_ANY]);
// Act
MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init];
[todoViewModel fetchData];
// Assert
XCTAssertEqual(todoViewModel.currentUiState, UIStateError);
}
驗證 UI 狀狀態為 Error
Test Case: 驗證網路路連線異異常
No Internet Time Out
/**
呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示
*/
- (void)testErrorState {
// Arrange - 模擬呼叫 API 錯誤
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}];
OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler:
([OCMArg invokeBlockWithArgs:error, nil])]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertTrue(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
/**
呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示
*/
- (void)testErrorState {
// Arrange - 模擬呼叫 API 錯誤
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}];
OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler:
([OCMArg invokeBlockWithArgs:error, nil])]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertTrue(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
Stub 呼叫的 API 發⽣生 error
/**
呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示
*/
- (void)testErrorState {
// Arrange - 模擬呼叫 API 錯誤
id mockApiService = OCMPartialMock([MKCApiService sharedApi]);
NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}];
OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler:
([OCMArg invokeBlockWithArgs:error, nil])]);
// Act - 載入畫⾯面
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertTrue(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
Table View 會被隱藏、loading 圖⽰示隱藏、Table View 裡的資料量量為 0
Test Case: 驗證畫⾯面載入中
/**
呼叫 API,載入中狀狀態顯⽰示
*/
- (void)testLoadingState {
// Arrange - 將狀狀態固定回傳 Loading
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)];
[mockTodoViewModel setDelegate:todoViewController];
todoViewController.todoViewModel = mockTodoViewModel;
// Act - 載入畫⾯面
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertFalse(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
/**
呼叫 API,載入中狀狀態顯⽰示
*/
- (void)testLoadingState {
// Arrange - 將狀狀態固定回傳 Loading
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)];
[mockTodoViewModel setDelegate:todoViewController];
todoViewController.todoViewModel = mockTodoViewModel;
// Act - 載入畫⾯面
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertFalse(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
Stub UI 的狀狀態為 Loading
/**
呼叫 API,載入中狀狀態顯⽰示
*/
- (void)testLoadingState {
// Arrange - 將狀狀態固定回傳 Loading
MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init];
id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]);
[OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)];
[mockTodoViewModel setDelegate:todoViewController];
todoViewController.todoViewModel = mockTodoViewModel;
// Act - 載入畫⾯面
[todoViewController view];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
// Assert - 驗證畫⾯面狀狀態,及資料數量量
XCTAssertEqual(todoViewController.tableView.alpha, 0.0);
XCTAssertFalse(todoViewController.activityIndicatorView.isHidden);
XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0);
}
Table View 會被隱藏、顯⽰示 loading 圖⽰示、Table View 裡的資料量量為 0
Test Case: 驗證 UI 反應⾏行行為
/**
點選 table view 的 cell 後,cell 會顯⽰示取消選取動畫
*/
- (void)testTapTableViewCell {
// Arrange - 模擬 table view
id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView);
// Act - 模擬使⽤用者點選 table view 的 cell
NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[mockTableView delegate] tableView:mockTableView
didSelectRowAtIndexPath:selectIndexPath];
// Assert - 取消選取有被執⾏行行
OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]);
}
/**
點選 table view 的 cell 後,cell 會顯⽰示取消選取動畫
*/
- (void)testTapTableViewCell {
// Arrange - 模擬 table view
id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView);
// Act - 模擬使⽤用者點選 table view 的 cell
NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[mockTableView delegate] tableView:mockTableView
didSelectRowAtIndexPath:selectIndexPath];
// Assert - 取消選取有被執⾏行行
OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]);
}
Mock ⼀一個 Table View,因為我們要驗證,取消選取 cell 的 method 是否有被呼叫
/**
點選 table view 的 cell 後,cell 會顯⽰示取消選取動畫
*/
- (void)testTapTableViewCell {
// Arrange - 模擬 table view
id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView);
// Act - 模擬使⽤用者點選 table view 的 cell
NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[mockTableView delegate] tableView:mockTableView
didSelectRowAtIndexPath:selectIndexPath];
// Assert - 取消選取有被執⾏行行
OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]);
}
點選 Table View 中第 1 個 Cell
/**
點選 table view 的 cell 後,cell 會顯⽰示取消選取動畫
*/
- (void)testTapTableViewCell {
// Arrange - 模擬 table view
id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView);
// Act - 模擬使⽤用者點選 table view 的 cell
NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[mockTableView delegate] tableView:mockTableView
didSelectRowAtIndexPath:selectIndexPath];
// Assert - 取消選取有被執⾏行行
OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]);
}
驗證 Table View 取消選取的 method 有被呼叫
https://github.com/markchangjz/offline-testing
More Test Cases
Demo
Summary
• 透過 WireMock 做⼀一個假的 Server,避免網路路不穩定
• 使⽤用 Stub 跟 Mock ⽅方法來來做 Unit Test,隔離掉網路路層
感謝聆聽  敬請指教
#Any test is better than no test
Reference
• How to run Swift UI tests with a mock API server - Marco Santa Dev
• How not to get desperate with MVVM implementation – Flawless App Stories – Medium
• Real World Mocking in Swift
• The complete guide to Network Unit Testing in Swift
• When to Mock
• Swift 開發:如何使⽤用 Xcode 7 進⾏行行單元測試
• 使⽤用 switch_case 重構程式碼
• 有效的單元測試 (PHP 版)
• Unit Test , UI Test , Jenkins (C.I.) 教學 — iOS – Jerry Wang – Medium
• Mocking HTTP requests with Nock – codeburst
iOS XCUITest
Test Corner #7 @ 2016/12
Page Object in XCUITest
Test Corner #2 @ 2016/02
iOS UI Testing in Xcode

Building stable testing by isolating network layer

  • 1.
    Building stable testingby isolating network layer Mark Chang - iOS Developer shea4a71@gmail.com Test Corner #16 @ Cardinal Blue (PicCollage)
  • 2.
    Motivation • 😭 執⾏行行⾃自動化測試,最後發現是網路路問題造成錯誤 •😞 測試因為執⾏行行環境不同⽽而造成失敗 • 🙄 還沒測到要測試的功能,測試就 GG 了了 • 😩 不同時間,執⾏行行結果不同...
  • 3.
  • 4.
  • 5.
    Outline • UI Test •Introduction to “WireMock" mock server tool • How to redirect API host • Unit Test • Stub and Mock (using OCMock library) • Implementing some test cases
  • 6.
  • 7.
  • 8.
  • 9.
    Solution • 建立⼀一個本機端的 MockServer,回傳罐頭資訊 • 測試資料可⾃自⾏行行掌握 • App 端只需修改連線的 API 串串接到 Mock Server • 所需的異異動較⼩小 • 降低 API Response 的等待時間,及網路路的不確定性
  • 10.
    WireMock • ⽤用來來模擬 API回應資料的⼯工具 (Download) • 執⾏行行在本機端 java -jar wiremock-standalone-2.17.0.jar --port 1234
  • 11.
    WireMock • 執⾏行行指令後,產出 __file跟 mappings 資料夾 • __file: API response 的 JSON 檔案 • mappings: 設定 URL 與 JSON 檔案的對應關係 • 詳細可參參考此 Link 中的 Mock Server Configuration
  • 12.
    Redirect URL toMock Server • 將 Production API URL 轉導⾄至 Mock Server API URL • ⽅方法 1 : Charles Proxy (Map Remote) • ⽅方法 2 : Xcode Configuration (.xcconfig) https://jsonplaceholder.typicode.com/todos http://localhost:1234/todos Production Mock Server
  • 13.
  • 14.
    Xcode Configuration • 透過Build Configuration 來來切換環境 • 詳細設定步驟可參參考此 Link
  • 15.
    Xcode Configuration - (NSString*)apiHost { return [[NSBundle mainBundle].infoDictionary[@"API HOST"] stringByReplacingOccurrencesOfString:@"" withString:@""]; } App 裡呼叫 API class 的 API HOST 位址,讀取設定檔裡的位址 API_HOST = https://jsonplaceholder.typicode.com API_HOST = http://localhost:1234 API_HOST = https://jsonplaceholder.typicode.com
  • 16.
  • 17.
  • 18.
  • 19.
    How do youtesting without making a real request ? Stub and Mock
  • 20.
    Stub and Mock •Stub • Fakes a response to method calls of an object class StubTimeMachineAPI: TimeMachineAPI { var videoUrl = "https://www.youtube.com/watch?v=SQ8aRKG9660" func getVideoFor(year: Int) -> String { return videoUrl } } Reference
  • 21.
    Stub and Mock •Mock • Let you check if a method call is performed or if a property is set class MockTimeMachine: TimeMachine { var timeTravelWasCalled = false func timeTravelTo(year: Int) { timeTravelWasCalled = true } } Reference
  • 22.
    Stub never faila unit test but a mock can What's the difference between a stub and mock? Reference
  • 23.
    Real World • 可能需要改程式架構,讓可測試性提⾼高 •Legacy code 可能改不動,或是改完產⽣生更更⾼高的風險 • 想到⼀一堆前置動作就放棄惹 😐
  • 24.
    Solution • 整合測試的 UnitTest • 測試的單位為⼀一個畫⾯面 (ViewController) • 有點把 Unit Test 當 UI Test ⽤用 😆 • 快速提升 Code Coverage,CP 值頗⾼高 • 先求有,再求好
  • 25.
    OCMock Library • ClassMock • Partial Mock • Stubbing methods that return objects id classMock = OCMClassMock([SomeClass class]); id partialMock = OCMPartialMock(anObject); OCMStub([mock someMethod]).andReturn(anObject);
  • 26.
  • 27.
    /** 驗證 Cell 顯⽰示的資料 */ -(void)testCellData { // Arrange - 讀取本機 JSON file,模擬 API 回傳資料 NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; // Assert - 驗證 Cell 顯⽰示資料 UITableView *todoTableView = todoViewController.tableView; [responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]]; XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]); UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; XCTAssertEqual(cell.accessoryType, accessoryType); }]; }
  • 28.
    /** 驗證 Cell 顯⽰示的資料 */ -(void)testCellData { // Arrange - 讀取本機 JSON file,模擬 API 回傳資料 NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; // Assert - 驗證 Cell 顯⽰示資料 UITableView *todoTableView = todoViewController.tableView; [responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]]; XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]); UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; XCTAssertEqual(cell.accessoryType, accessoryType); }]; } Stub API 回傳的資料 直接讀取本機端 API 回傳的 JSON 資料 ( todos.json )
  • 29.
    /** 驗證 Cell 顯⽰示的資料 */ -(void)testCellData { // Arrange - 讀取本機 JSON file,模擬 API 回傳資料 NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; // Assert - 驗證 Cell 顯⽰示資料 UITableView *todoTableView = todoViewController.tableView; [responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]]; XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]); UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; XCTAssertEqual(cell.accessoryType, accessoryType); }]; } 初始化畫⾯面
  • 30.
    /** 驗證 Cell 顯⽰示的資料 */ -(void)testCellData { // Arrange - 讀取本機 JSON file,模擬 API 回傳資料 NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; // Assert - 驗證 Cell 顯⽰示資料 UITableView *todoTableView = todoViewController.tableView; [responseObject enumerateObjectsUsingBlock:^(id Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { UITableViewCell *cell = [todoTableView.dataSource tableView:todoTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0]]; XCTAssertEqualObjects(cell.textLabel.text, obj[@"title"]); UITableViewCellAccessoryType accessoryType = [obj[@"completed"] boolValue] ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; XCTAssertEqual(cell.accessoryType, accessoryType); }]; } 驗證 Cell 旁的 checkmark驗證 Cell 上的字串串
  • 31.
    Refactor • 有了了初步的 UnitTest 就可以來來重構程式 • 把 MVC (Model-View-Controller) 架構改⽤用 MVVM (Model-View-ViewModel) 架構
  • 32.
    - (void)testFetchDataSuccessful { NSBundle*bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [mockTodoViewModel setExpectationOrderMatters:YES]; // 以下狀狀態不會被執⾏行行 OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]); // 以下狀狀態會依序被執⾏行行 OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]); OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]); [mockTodoViewModel fetchData]; OCMVerifyAll(mockTodoViewModel); XCTAssertEqual([mockTodoViewModel numberOfCells], 5); MKCTodoCellViewModel *firstTodoCellViewModel = [mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1"); XCTAssertFalse(firstTodoCellViewModel.completed); }
  • 33.
    - (void)testFetchDataSuccessful { NSBundle*bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [mockTodoViewModel setExpectationOrderMatters:YES]; // 以下狀狀態不會被執⾏行行 OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]); // 以下狀狀態會依序被執⾏行行 OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]); OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]); [mockTodoViewModel fetchData]; OCMVerifyAll(mockTodoViewModel); XCTAssertEqual([mockTodoViewModel numberOfCells], 5); MKCTodoCellViewModel *firstTodoCellViewModel = [mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1"); XCTAssertFalse(firstTodoCellViewModel.completed); } Stub API 回傳的資料 直接讀取本機端 API 回傳的 JSON 資料 ( todos.json )
  • 34.
    - (void)testFetchDataSuccessful { NSBundle*bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [mockTodoViewModel setExpectationOrderMatters:YES]; // 以下狀狀態不會被執⾏行行 OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]); // 以下狀狀態會依序被執⾏行行 OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]); OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]); [mockTodoViewModel fetchData]; OCMVerifyAll(mockTodoViewModel); XCTAssertEqual([mockTodoViewModel numberOfCells], 5); MKCTodoCellViewModel *firstTodoCellViewModel = [mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1"); XCTAssertFalse(firstTodoCellViewModel.completed); } 先 Loading 再 Finish 驗證 function 執⾏行行順序 不會進入 Error 狀狀態 呼叫 API
  • 35.
    - (void)testFetchDataSuccessful { NSBundle*bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"todos" ofType:@"json"]; NSData *data = [NSData dataWithContentsOfFile:filePath]; NSArray *responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, responseObject, nil]) failureHandler:OCMOCK_ANY]); id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [mockTodoViewModel setExpectationOrderMatters:YES]; // 以下狀狀態不會被執⾏行行 OCMReject([mockTodoViewModel setCurrentUiState:UIStateError]); // 以下狀狀態會依序被執⾏行行 OCMExpect([mockTodoViewModel setCurrentUiState:UIStateLoading]); OCMExpect([mockTodoViewModel setCurrentUiState:UIStateFinish]); [mockTodoViewModel fetchData]; OCMVerifyAll(mockTodoViewModel); XCTAssertEqual([mockTodoViewModel numberOfCells], 5); MKCTodoCellViewModel *firstTodoCellViewModel = [mockTodoViewModel cellViewModelAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; XCTAssertEqualObjects(firstTodoCellViewModel.title, @"mock test 1"); XCTAssertFalse(firstTodoCellViewModel.completed); } 驗證資料量量正確,及 Table View 上第⼀一個 Cell 顯⽰示的資料符合預期
  • 36.
    如何測試? • API 回傳不合法資料 •網路路連線異異常 • 畫⾯面載入中 • 驗證 UI 反應⾏行行為
  • 37.
    Error conditions andexceptions are nearly impossible to test without mocks that can simulate those errors.
  • 38.
    Test Case: 驗證API 回傳不合法資料
  • 39.
    #pragma mark -API 回傳異異常資料 - (void)testApiResponseInvalidData { // Arrange id invalidResponseObject = @[@"a", @"b", @"c"]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, invalidResponseObject, nil]) failureHandler:OCMOCK_ANY]); // Act MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init]; [todoViewModel fetchData]; // Assert XCTAssertEqual(todoViewModel.currentUiState, UIStateError); }
  • 40.
    #pragma mark -API 回傳異異常資料 - (void)testApiResponseInvalidData { // Arrange id invalidResponseObject = @[@"a", @"b", @"c"]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, invalidResponseObject, nil]) failureHandler:OCMOCK_ANY]); // Act MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init]; [todoViewModel fetchData]; // Assert XCTAssertEqual(todoViewModel.currentUiState, UIStateError); } Stub API 回應不合法資料 (如: 不符的資料格式)
  • 41.
    #pragma mark -API 回傳異異常資料 - (void)testApiResponseInvalidData { // Arrange id invalidResponseObject = @[@"a", @"b", @"c"]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, invalidResponseObject, nil]) failureHandler:OCMOCK_ANY]); // Act MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init]; [todoViewModel fetchData]; // Assert XCTAssertEqual(todoViewModel.currentUiState, UIStateError); } 呼叫 API
  • 42.
    #pragma mark -API 回傳異異常資料 - (void)testApiResponseInvalidData { // Arrange id invalidResponseObject = @[@"a", @"b", @"c"]; id mockApiService = OCMPartialMock([MKCApiService sharedApi]); OCMStub([mockApiService fetchTodoListWithSuccessHandler: ([OCMArg invokeBlockWithArgs:OCMOCK_ANY, invalidResponseObject, nil]) failureHandler:OCMOCK_ANY]); // Act MKCTodoViewModel *todoViewModel = [[MKCTodoViewModel alloc] init]; [todoViewModel fetchData]; // Assert XCTAssertEqual(todoViewModel.currentUiState, UIStateError); } 驗證 UI 狀狀態為 Error
  • 43.
  • 44.
    /** 呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示 */ -(void)testErrorState { // Arrange - 模擬呼叫 API 錯誤 id mockApiService = OCMPartialMock([MKCApiService sharedApi]); NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}]; OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler: ([OCMArg invokeBlockWithArgs:error, nil])]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertTrue(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); }
  • 45.
    /** 呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示 */ -(void)testErrorState { // Arrange - 模擬呼叫 API 錯誤 id mockApiService = OCMPartialMock([MKCApiService sharedApi]); NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}]; OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler: ([OCMArg invokeBlockWithArgs:error, nil])]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertTrue(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); } Stub 呼叫的 API 發⽣生 error
  • 46.
    /** 呼叫 API 發⽣生錯誤時,錯誤狀狀態顯⽰示 */ -(void)testErrorState { // Arrange - 模擬呼叫 API 錯誤 id mockApiService = OCMPartialMock([MKCApiService sharedApi]); NSError *error = [NSError errorWithDomain:@"test.error" code:123 userInfo:@{}]; OCMStub([mockApiService fetchTodoListWithSuccessHandler:OCMOCK_ANY failureHandler: ([OCMArg invokeBlockWithArgs:error, nil])]); // Act - 載入畫⾯面 MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertTrue(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); } Table View 會被隱藏、loading 圖⽰示隱藏、Table View 裡的資料量量為 0
  • 47.
  • 48.
    /** 呼叫 API,載入中狀狀態顯⽰示 */ - (void)testLoadingState{ // Arrange - 將狀狀態固定回傳 Loading MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)]; [mockTodoViewModel setDelegate:todoViewController]; todoViewController.todoViewModel = mockTodoViewModel; // Act - 載入畫⾯面 [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertFalse(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); }
  • 49.
    /** 呼叫 API,載入中狀狀態顯⽰示 */ - (void)testLoadingState{ // Arrange - 將狀狀態固定回傳 Loading MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)]; [mockTodoViewModel setDelegate:todoViewController]; todoViewController.todoViewModel = mockTodoViewModel; // Act - 載入畫⾯面 [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertFalse(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); } Stub UI 的狀狀態為 Loading
  • 50.
    /** 呼叫 API,載入中狀狀態顯⽰示 */ - (void)testLoadingState{ // Arrange - 將狀狀態固定回傳 Loading MKCTodoViewController *todoViewController = [[MKCTodoViewController alloc] init]; id mockTodoViewModel = OCMPartialMock([[MKCTodoViewModel alloc] init]); [OCMStub([mockTodoViewModel currentUiState]) andReturnValue:OCMOCK_VALUE(UIStateLoading)]; [mockTodoViewModel setDelegate:todoViewController]; todoViewController.todoViewModel = mockTodoViewModel; // Act - 載入畫⾯面 [todoViewController view]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; // Assert - 驗證畫⾯面狀狀態,及資料數量量 XCTAssertEqual(todoViewController.tableView.alpha, 0.0); XCTAssertFalse(todoViewController.activityIndicatorView.isHidden); XCTAssertEqual([todoViewController.tableView numberOfRowsInSection:0], 0); } Table View 會被隱藏、顯⽰示 loading 圖⽰示、Table View 裡的資料量量為 0
  • 51.
    Test Case: 驗證UI 反應⾏行行為
  • 52.
    /** 點選 table view的 cell 後,cell 會顯⽰示取消選取動畫 */ - (void)testTapTableViewCell { // Arrange - 模擬 table view id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView); // Act - 模擬使⽤用者點選 table view 的 cell NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; [[mockTableView delegate] tableView:mockTableView didSelectRowAtIndexPath:selectIndexPath]; // Assert - 取消選取有被執⾏行行 OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]); }
  • 53.
    /** 點選 table view的 cell 後,cell 會顯⽰示取消選取動畫 */ - (void)testTapTableViewCell { // Arrange - 模擬 table view id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView); // Act - 模擬使⽤用者點選 table view 的 cell NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; [[mockTableView delegate] tableView:mockTableView didSelectRowAtIndexPath:selectIndexPath]; // Assert - 取消選取有被執⾏行行 OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]); } Mock ⼀一個 Table View,因為我們要驗證,取消選取 cell 的 method 是否有被呼叫
  • 54.
    /** 點選 table view的 cell 後,cell 會顯⽰示取消選取動畫 */ - (void)testTapTableViewCell { // Arrange - 模擬 table view id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView); // Act - 模擬使⽤用者點選 table view 的 cell NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; [[mockTableView delegate] tableView:mockTableView didSelectRowAtIndexPath:selectIndexPath]; // Assert - 取消選取有被執⾏行行 OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]); } 點選 Table View 中第 1 個 Cell
  • 55.
    /** 點選 table view的 cell 後,cell 會顯⽰示取消選取動畫 */ - (void)testTapTableViewCell { // Arrange - 模擬 table view id mockTableView = OCMPartialMock([[MKCTodoViewController alloc] init].tableView); // Act - 模擬使⽤用者點選 table view 的 cell NSIndexPath *selectIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; [[mockTableView delegate] tableView:mockTableView didSelectRowAtIndexPath:selectIndexPath]; // Assert - 取消選取有被執⾏行行 OCMVerify([mockTableView deselectRowAtIndexPath:selectIndexPath animated:YES]); } 驗證 Table View 取消選取的 method 有被呼叫
  • 56.
  • 57.
  • 58.
    Summary • 透過 WireMock做⼀一個假的 Server,避免網路路不穩定 • 使⽤用 Stub 跟 Mock ⽅方法來來做 Unit Test,隔離掉網路路層
  • 59.
  • 60.
    Reference • How torun Swift UI tests with a mock API server - Marco Santa Dev • How not to get desperate with MVVM implementation – Flawless App Stories – Medium • Real World Mocking in Swift • The complete guide to Network Unit Testing in Swift • When to Mock • Swift 開發:如何使⽤用 Xcode 7 進⾏行行單元測試 • 使⽤用 switch_case 重構程式碼 • 有效的單元測試 (PHP 版) • Unit Test , UI Test , Jenkins (C.I.) 教學 — iOS – Jerry Wang – Medium • Mocking HTTP requests with Nock – codeburst
  • 61.
    iOS XCUITest Test Corner#7 @ 2016/12 Page Object in XCUITest Test Corner #2 @ 2016/02 iOS UI Testing in Xcode