Unit Testing: Special Cases

Post on 27-Dec-2014

109 views 2 download

description

The 4th Dnepropetrovsk iOS Practice Leaders Community Meet-Up, which took place onThursday, September 25. Maxim Koshtenko, an iOS developer with 4+ years of experience in the area, held a presentation in which he told: - about the most widespread problems which appear while writing tests and how to solve them; - how to cover controllers with tests correctly and what should be visible in interface; - why tests do not work for block-based and asynchronous code and how we can fix this; - how to write tests for Core Data models; - many other useful and interesting tips and tricks. The presentation will be interesting for all iOS developers.

transcript

Unit TestingSpecial Cases

How to Test

UIViewControllers

How to TestUnit Testing

What Is Testing For? 

UIViewControllers Block-based code !

How to TestUnit Testing

What Is Testing For? 

UIViewControllers Block-based code CoreData

How to TestUnit Testing

What Is Testing For? 

UIViewController

IBOutlets & IBActions viewDidLoad dealloc UINavigationController

UIViewController

IBOutlets & IBActions

Outlet is connected

- (void)setUp { [super setUp]; sut = [ConverterViewController new]; } !- (void)tearDown { sut = nil; [super tearDown]; }

Is Outlet Connected?

- (void)setUp { [super setUp]; sut = [ConverterViewController new]; } !- (void)tearDown { sut = nil; [super tearDown]; } !- (void)testThatTextFieldOutletIsConnected { XCTAssertNotNil(sut.textField, @"outlet should be connected"); }

Is Outlet Connected?

- (void)setUp { [super setUp]; sut = [ConverterViewController new]; } !- (void)tearDown { sut = nil; [super tearDown]; } !- (void)testThatTextFieldOutletIsConnected { XCTAssertNotNil(sut.textField, @"outlet should be connected"); }

Is Outlet Connected?

- (void)setUp { [super setUp]; sut = [ConverterViewController new]; } !- (void)tearDown { sut = nil; [super tearDown]; } !- (void)testThatTextFieldOutletIsConnected { [sut view]; XCTAssertNotNil(sut.textField, @"outlet should be connected"); }

Is Outlet Connected?

IBOutlets & IBActions

Outlet has a right action.

- (void)testButtonActionBinding { [sut view]; NSArray* acts = [sut.button actionsForTarget:sut forControlEvent:UIControlEventTouchUpInside]; XCTAssert([acts containsObject:@"onButton:"], @"should use correct action"); }

Is Action Connected?

IBOutlets & IBActions

The action does the right things.

viewDidLoad

Unit testing of a view controller nearly always means writing the view controller methods differently

viewDidLoad

- should call helper methods

viewDidLoad

- should call helper methods - each of the helper methods should

do just one thing (SOLID principles)

viewDidLoad

- should call helper methods - each of the helper methods should

do just one thing (SOLID principles) - write tests for each of the helper

methods

viewDidLoad

- should call helper methods - each of the helper methods should

do just one thing (SOLID principles) - write tests for each of the helper

methods - test viewDidLoad calls helper

methods (partial mock)

dealloc

setUp tearDown zombie

dealloc

❓hook dealloc method of SUT when setup

dealloc

❓hook dealloc method of SUT when setup ❓record calling of the hook

dealloc

❓hook dealloc method of SUT when setup ❓record calling of the hook ❓verify if hook is called after teardown

Aspects

/// Adds a block of code before/instead/after the current `selector` for a specific instance. - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; !/// Called after the original implementation (default) AspectPositionAfter, !/// Will replace the original implementation. AspectPositionInstead, !/// Called before the original implementation. AspectPositionBefore,

Aspects

dealloc

✅ hook dealloc method of SUT when setup ❓ record calling of the hook ❓ verify if hook is called after teardown

Aspects

dealloc

✅ hook dealloc method of SUT when setup ✅ record calling of the hook ❓ verify if hook is called after teardown

Aspects

instance var

dealloc

✅ hook dealloc method of SUT when setup ✅ record calling of the hook ✅ verify if hook is called after teardown

Aspects

instance var

XCTAssert

@interface ConverterViewControllerTests : XCTestCase { ConverterViewController* sut; BOOL _sutDeallocated; } @end !- (void)setUp { [super setUp]; sut = [ConverterViewController new]; _sutDeallocated = NO; [sut aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){ _sutDeallocated = YES; } error:nil]; } !- (void)tearDown { sut = nil; XCTAssertTrue(_sutDeallocated, @"SUT is not deallocated"); [super tearDown]; }

dealloc

@interface } @end !- ( [ !!!!!} !- ( [}

dealloc!!!!!!!!!! [sut aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){ _sutDeallocated = YES; } error:nil];

UINavigationController

- test push view controller - test pop view controller

UINavigationController

UINavigationController

But

UINavigationController

But

@interface UIViewController (UINavigationControllerItem) ! @property(nonatomic,readonly,retain) UINavigationController* navigationController; ! @end

-(void)testTappingDetailsShouldDisplayDetails { UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:sut]; id mockNav = [OCMockObject partialMockForObject:nav]; [[mockNav expect] pushViewController:[OCMArg any] animated:YES]; [sut onShowDetailsButton:nil]; [mockNav verify]; }

UINavigationController

Choosing not to test view controllers is the decision not to test most of your code.

UIViewController

Testing simple things is simple, and testing complex things is complex

UIViewController

Block-based code

Why does test fail?

Block-based code

! typedef void(^CompletionHandler)(NSArray * result); ! - (void)runAsyncCode:(CompletionHandler)completion { dispatch_async(dispatch_get_main_queue(), ^{ completion(nil); }); }

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

Block-based code

What can we do?

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert !!!! ! XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:0.1]; while([timeout timeIntervalSinceNow] > 0 && hasCalledBack == NO) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeout]; } XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

- (void)testRunAsyncCode { // arrange __block BOOL hasCalledBack = NO; ! // act [sut runAsyncCode:^(NSArray *result) { hasCalledBack = YES; }]; // assert NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:0.1]; while([timeout timeIntervalSinceNow] > 0 && hasCalledBack == NO) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeout]; } XCTAssert(hasCalledBack, @"Test timed out"); }

Block-based code

Block-based codeAlso we can use:

- (void)verifyWithDelay:(NSTimeInterval)delay { NSTimeInterval step = 0.01; while(delay > 0) { if([expectations count] == 0) break; NSDate* until = [NSDate dateWithTimeIntervalSinceNow:step]; [[NSRunLoop currentRunLoop] runUntilDate:until]; delay -= step; step *= 2; } [self verify]; }

OCMock

CoreData

CoreData

As long as you don't put business logic in your models, you don't have

to test them.

CoreData

creates setters & getters in run-time

CoreData

creates setters & getters in run-time

we can’t mock CoreData models

CoreData

What can we do?

CoreData- create protocol that has all model’s

properties defined

CoreData- create protocol that has all model’s

properties defined - conform NSManagedObject to the

protocol

CoreData- create protocol that has all model’s

properties defined - conform NSManagedObject to the

protocol - create NSObject model just for

testing, conforms to the protocol and @synthesize properties

CoreData

Protocol

Model<Protocol> TestModel<Protocol>

CoreDataProtocol

Model<Protocol> TestModel<Protocol>

CoreData

Have a better solution?

CoreDataCreate own CoreData stack for each

test-case

- (void)setUp { [super setUp]; NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"SimpleInvoice" withExtension:@“momd"]; ! NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; ! NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; ! XCTAssertNotNil([psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL], @"Should be able to add in-memory store”); ! _moc = [[NSManagedObjectContext alloc] init]; _moc.persistentStoreCoordinator = psc; }

CoreData

- (void{ [ ! initWithContentsOfURL! initWithManagedObjectModel!!!!!!!! }

CoreData

- (void)setUp { !!!!!!!!!! XCTAssertNotNil([psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL], @"Should be able to add in-memory store”); !!!}

CoreData

Advantages?

CoreDataAdvantages?

- no additional classes

CoreDataAdvantages?

- no additional classes - no dependence on external state

CoreDataAdvantages?

- no additional classes - no dependence on external state - close approximation to the

application environment

CoreDataAdvantages?

- no additional classes - no dependence on external state - close approximation to the

application environment - we are able to create base test

class with a stack and subclass it where we need

CoreDataAdvantages?

- no additional classes

- no dependence on external state

- close approximation to the application environment

- we are able to create base test class with a stack and subclass it where we need

- (void)testFullNameReturnsСorrectString { Person* ps; ps = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:_moc]; ps.firstName = @"Tom"; ps.lastName = @“Lol"; ! STAssertTrue([[ps fullName] isEqualToString:@"Lol Tom"], @"should have matched full name"); }

CoreData

CoreData

- test model’s additional business-logic - test ManagedObjectModel for an

entity - create and use models as a mocks

✅ UIViewControllers ✅ Block-based code ✅ CoreData

How to Test

Video is coming!

Demo Codehttps://github.com/maksumko/ConverterApp"!

maksum.ko

contact me:

Sourceshttp://www.amazon.com/Test-Driven-iOS-Development-Developers-Library/dp/0321774183 !http://www.objc.io/issue-1/testing-view-controllers.html http://www.silverbaytech.com/2013/02/25/ios-testing-part-3-testing-view-controller/ http://iosunittesting.com/unit-testing-view-controllers/ http://blog.carbonfive.com/2010/03/10/testing-view-controllers/ !http://iosunittesting.com/how-to-unit-test-completion-blocks/ !http://iosunittesting.com/unit-testing-core-data/ http://ashfurrow.com/blog/unit-testing-with-core-data-models http://www.sicpers.info/2010/06/template-class-for-unit-testing-core-data-entities/ http://iamleeg.blogspot.com/2010/01/unit-testing-core-data-driven-apps-fit.html