ios testing
TRANSCRIPT
iOS TestingDerrick Chao
Frameworks
• XCTest
• OCMock
• KIF
XCTest• Xcode 內建測試框架
• ⽅方法以 test 開頭
- (void)testExample { // This is an example of a functional test case. XCTAssert(YES, @"Pass"); XCTAssert(1+1==2); XCTAssertTrue(1+1==2); XCTAssertFalse(1+1==3); }
• XCTAssert XCTAssert(expression, …); XCTAssertTrue(expression, …); XCTAssertFalse(expression, ...);
Unit Test Example- (void)testValidatePassword { NSString *password1 = @"AbCd1234"; NSString *password2 = @"AbCd12"; NSString *password3 = @"ABCD"; NSString *password4 = @"AbCd1"; NSString *password5 = @""; XCTAssertTrue([Helper validatePassword:password1]); XCTAssertTrue([Helper validatePassword:password2]); XCTAssertFalse([Helper validatePassword:password3]); XCTAssertFalse([Helper validatePassword:password4]); XCTAssertFalse([Helper validatePassword:password5]); }
Test Async Function Example
• 寄⼀一則 message 給另⼀一個 user 的 function
• async
• block callback
Test Async Function Example
- (void)testSendMessageWithNilMessage { XCTestExpectation *expectation =
[self expectationWithDescription:@"completion should be called"]; [Message sendMessage:nil recipientInfo:nil completion:^(id responseObject, NSError *error) { [expectation fulfill]; XCTAssertNotNil(error); XCTAssert([error.userInfo[@"message"] isEqualToString:@"message can't be nil"]); }]; [self waitForExpectationsWithTimeout:2 handler:nil]; }
Singleton// LoopdManager.h file + (instancetype)sharedManager;
// LoopdManager.m file + (instancetype)sharedManager { static dispatch_once_t onceToken; static LoopdManager *sharedInstance; dispatch_once(&onceToken, ^{ sharedInstance = [[LoopdManager alloc] init]; }); return sharedInstance; }
Test Singleton Example
- (void)testSharedManager { LoopdManager *manager1 = [LoopdManager sharedManager]; LoopdManager *manager2 = [LoopdManager sharedManager]; LoopdManager *manager3 = [LoopdManager new]; XCTAssertNotNil(manager1); XCTAssertNotNil(manager2); XCTAssertEqual(manager1, manager2); XCTAssertNotEqual(manager1, manager3); }
OCMock• 建造⼀一個虛擬的對象物件,可以⾃自訂回傳值
- (void)testMock { id mockUserInfo = OCMClassMock([UserInfo class]); // fullname should be nil for now XCTAssertNil([mockUserInfo fullname]); // make a custom return value OCMStub([mockUserInfo fullname]).andReturn(@"David"); // fullname should be David now XCTAssert([[mockUserInfo fullname] isEqualToString:@"David"]); }
OCMock Example (1)ListViewController 有個 collection view viewDidLoad 時從 server 上 find all SomeModel, 並 show 出來
// in SomeModel.h file@interface SomeModel : ModelObject
+ (void)findAllByCurrentUserId:(NSString *)currentUserId completion:(ArrayResultBlock)completion;
@end
OCMock Example (2)- (void)testCollectionViewCellNumber { id mockSomeModel = OCMClassMock([SomeModel class]); OCMStub([mockSomeModel findAllRelationshipsByCurrentUserId:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { ArrayResultBlock completion; [invocation getArgument:&completion atIndex: 3]; NSArray *someModels = @[@“1”, @“2”, @“3”, @“4”, @“5”]; completion(someModels, nil); }); ListViewController *listVC = [ListViewController new]; listVC.view; NSInteger num = [listVC collectionView:nil numberOfItemsInSection:0]; XCTAssert(num == 5); XCTAssert(num != 6); }
KIF• KIF iOS Integration Testing Framework
KIF Example@interface UITests : KIFTestCase
@end
@implementation UITests
- (void)testLoginSteps { [tester tapViewWithAccessibilityLabel:@"regular_button"]; [tester enterText:@"[email protected]" intoViewWithAccessibilityLabel:@"emailTextField"]; [tester enterText:@"abcdefg" intoViewWithAccessibilityLabel:@"passwordTextField"]; [tester tapViewWithAccessibilityLabel:@“checkmark_button"]; }
@end
MVC & MVP
MVC• view 可透過 controller 取得 model 資料
• 顯⽰示 UI 的邏輯寫在 view 裡⾯面
• 耦合較⾼高
MVC & MVP
MVP• 由 MVC 演變⽽而來
• View 無法直接取得 model, 資料傳遞需透過 presenter
• 耦合較低
MVP 的優點• view 和 model 完全分離, 修改
view 不會影響到 model
• ⼀一個 presenter 可以⽤用於多個 view, 不需改變 presenter 邏輯
• model 取得的邏輯在 presenter 內, 所以不需要 view 即可做 unit test
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; // config cell Car *car = self.cars[indexPath.item]; cell.makeLabel.text = car.make; cell.modelLabel.text = car.model;
cell.yearLabel.text = car.year; cell.detailLabel.text = car.detail;
return cell; }
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; // config cell Car *car = self.cars[indexPath.item]; cell.car = car;
[cell showUI]; return cell; }
Test Cell- (void)testCollectionViewCellNumber { id mockSomeModel = OCMClassMock([SomeModel class]); OCMStub([mockSomeModel findAllRelationshipsByCurrentUserId:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { ArrayResultBlock completion; [invocation getArgument:&completion atIndex: 3]; NSArray *someModels = @[mod1, mod2, mod3, mod4, mod5]; completion(someModels, nil); }); ListViewController *listVC = [ListViewController new]; listVC.view; NSInteger num = [listVC collectionView:nil numberOfItemsInSection:0]; XCTAssert(num == 5); XCTAssert(num != 6);
UICollectionViewCell *cell = [contactListVC collectionView:nil cellForItemAtIndexPath:0];
}
為什麼 Apple 說 iOS app 是 MVC?
• 在開發 iOS app 的時候, view 和 model 並沒有直接的關係
• 不會在 view 中寫取得 model 的 code
Spike Solution
Spike Solution
• 不確定功能如何設計
• 不確定此功能是否符合需求
Spike Solution (2)• A spike solution is dirty and quick.
Code just enough to get the answer you need.
• Throw it away when you’re done with it.
Spike Solution (3)• 開個新的 branch
• 在這個 branch 內測試新功能
• 結束以後切回原本 branch,不要 merge 回去,測試的 code 不應該出現在 production code 中
TDD with Xcode• 需求: 想要有個做加法運算的 Helper
TDD with Xcode (2)• 建出缺少的類別和⽅方法
TDD with Xcode (3)• 可以將 Xcode 選擇分割畫⾯面, 並將右邊畫⾯面選 Test Classes
TDD with Xcode (4)• command-U
TDD with Xcode (5)• 驗證加法運算是否正確
TDD with Xcode (6)• 補完 function, 然後再測試⼀一次
⼀一開始我對 TDD 的感覺
Testing or TDD 的好處• test => spec
• 確保新的 code 不會造成舊的 code 出現 bug
• 同伴或下⼀一個接⼿手的⼈人,不會⽤用錯誤的⽅方式使⽤用。修改舊的 code 也較有信⼼心
• 幫助程式寫得更好更乾淨
• 提⾼高測試的涵蓋率
使⽤用 TDD 之後
使⽤用 TDD 之後
使⽤用 TDD 之後
The End
備註• sorry 當天我忘了 Q&A 時間, 所以有任何問題都可以 email 給我 => [email protected] , 我們可以互相討論!
• 或者可以加⼊入 iOS Developer Slack 群組, 我的 id是 wasdplayer, 可以直接私訊我!裡⾯面有有超多的 iOS 開發者交流各種訊息, 是個不錯的地⽅方!
• http://ios-developers.io 按這裡邀請你⾃自⼰己加⼊入 Slack 群組!
參考資料• http://iosunittesting.com
• http://stackoverflow.com/questions/1053406/why-does-apple-say-iphone-apps-use-mvc
• http://blog.sanc.idv.tw/2011/09/uimvp-passive-view.html