ios unit testing ii
TRANSCRIPT
iOS Unit testing II
Liyao Chen @ KKBOX
Outlines
• External dependency
• 透過 Protocol 依賴注⼊入解套
• 透過隔離框架解套
External dependency
–Roy Osherove
是系統中的⼀一個對象, 被測試的程式碼會與其互動,但是你不能控制它。
External dependency
(⾦金迎 譯)
External dependency
(
External dependency
你跟著師⽗父去幫別⼈人佈線安裝電燈,師⽗父三兩下就搞定了,然後請你確認有沒有裝好,請問你會怎麼做?
打開開關看看電燈有沒有跟著亮再關上開關看電燈有沒有跟著暗
External dependency
⼀一個⽉月後客⼾戶打來說電燈打不開,請你們去檢查,你第⼀一時間會想到的問題是什麼?
可能開關壞掉了?
可能電線沒接好?
可能電線壞掉了?
External dependency
可能電線壞掉了?
可能開關壞掉了?
可能電線沒接好?
External dependency
External dependency
static NSTimeInterval const yearInterval = 60*60*24*30*12;
@interface LCWire () @property (strong, nonatomic) NSDate *expiryDate; @end
@implementation LCWire
- (instancetype)init { if(self = [super init]){
// 製造⽇日期為 2010/10/10 NSDateComponents *comps = [[NSDateComponents alloc] init]; [comps setDay:10]; [comps setMonth:10]; [comps setYear:2010]; NSDate *manufacturedDate = [[NSCalendar currentCalendar] dateFromComponents:comps]; // 有效期限為兩年 _expiryDate = [manufacturedDate dateByAddingTimeInterval:yearInterval*2]; } return self; }
- (BOOL)isExpired { return [[NSDate date] timeIntervalSinceReferenceDate] > [self.expiryDate timeIntervalSinceReferenceDate]; }
@end
Break dependency
Break dependency
Dummy class
Fake
Dummy class
A class that implements an interface but contains fixed data and no logic.
– Sangdol
Dummy class
DEMO1. Extract protocol
2. Use protocol injection
3. Dummy class
Dummy class// LCWire.h @protocol Expirable <NSObject> - (BOOL)isExpired; @end
@interface LCWire : NSObject <Expirable> - (BOOL)isExpired; @end
// LCRoomTests.m // 永遠不會過期的電線 @interface LCNeverExpiredWire : NSObject <Expirable> @end @implementation LCNeverExpiredWire - (BOOL)isExpired { return NO; } @end
@interface LCRoomTests : XCTestCase @end @implementation LCRoomTests
- (void)testLightOnInit { id<Expirable> wire = [[LCNeverExpiredWire alloc] init]; LCRoom *room = [[LCRoom alloc] initWithLight:YES wire:wire]; XCTAssertTrue(room.isLight); } @end
Dummy class
- (void)testLightOnInit { LCWire *wire = [[LCWire alloc] init]; LCRoom *room = [[LCRoom alloc] initWithLight:YES wire:wire]; XCTAssertTrue(room.isLight); }
- (void)testLightOnInit { id<Expirable> wire = [[LCNeverExpiredWire alloc] init]; LCRoom *room = [[LCRoom alloc] initWithLight:YES wire:wire]; XCTAssertTrue(room.isLight); }
Dummy class
Break dependency
那那些不能透過軟體設計控制的怎麼辦?
Break dependency
Mock
OCMock
Mock object
Mock - PartialMock()
- (void)testLightOnInit { // Mock ⼀一個永遠不會過期的電線 LCWire *wire = [[LCWire alloc] init]; id mockWire = OCMPartialMock(wire); OCMStub([mockWire isExpired]).andReturn(NO); LCRoom *room = [[LCRoom alloc] initWithLight:YES wire:wire]; XCTAssertTrue(room.isLight); }
Mock
1. Mock [NSDate date]
2. Mock [NSUserDefault standardDefault]
3. Mock session
4. Mock different user in a test case
Mock singleton- (void)testMockDate { NSDate *now = [NSDate date]; NSDateComponents *comps = [[NSDateComponents alloc] init]; [comps setDay:10]; [comps setMonth:10]; [comps setYear:2010]; NSDate *speficDate = [[NSCalendar currentCalendar] dateFromComponents:comps]; id mockDate = OCMClassMock([NSDate class]); OCMStub([mockDate date]).andReturn(speficDate); // Mock [NSDate date] 讓他回傳指定⽇日期, 也就是 Mock 現在時間 NSLog(@"now: %@, mockDate: %@",now, [NSDate date]); }
- (void)testMockUserdefault { NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"userName"]; id mockUserDefault = OCMPartialMock([NSUserDefaults standardUserDefaults]); OCMStub([mockUserDefault objectForKey:@"userName"]).andReturn(@"Liyao Chen"); NSString *mockName = [[NSUserDefaults standardUserDefaults] objectForKey:@"userName"]; NSLog(@"name: %@, mockName: %@", name, mockName); }
Summary
• 發現程式缺陷才能解決
• 善⽤用依賴注⼊入改善程式架構
• 當測試有多個Mock時重新檢視設計
QnA
• API測試不是後端的⼯工作嗎?
• 什麼東⻄西⼀一定要測?
We are hiring!
@ KKBOX
Reference
• http://stackoverflow.com/questions/346372/whats-the-difference-between-faking-mocking-and-stubbing
Other links
• Sample codehttps://github.com/gliyao/LCUnitTestsExample