The story about why unit tests matter.
Чистыеunit-тесты
View Slide
What makes a clean test?Three things. Readability, readability, and readability.Robert C. Martin, «Clean Code»
#добришко
- (void)testLoadAlbums {NSError *err1 = [NSError errorWithDomain:@"" code:123 userInfo:nil];OCMStub([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]).andDo(block);[self waitForExpectationsWithTimeout:1. handler:^(NSError *localError) {OCMVerify([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]);}];}4
- (void)testLoadAlbums {NSError *err1 = [NSError errorWithDomain:@"" code:123 userInfo:nil];OCMStub([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]).andDo(block);[self waitForExpectationsWithTimeout:1. handler:^(NSError *localError) {OCMVerify([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]);}];}5
- (void)testLoadAlbums {NSError *err1 = [NSError errorWithDomain:@"" code:123 userInfo:nil];OCMStub([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]).andDo(block);[self waitForExpectationsWithTimeout:1. handler:^(NSError *localError) {OCMVerify([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]);}];}6
- (void)testLoadAlbums {NSError *err1 = [NSError errorWithDomain:@"" code:123 userInfo:nil];OCMStub([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]).andDo(block);[self waitForExpectationsWithTimeout:1. handler:^(NSError *localError) {OCMVerify([self.provider getAlbumsWithResultBlock:OCMOCK_ANY]);}];}7
- (void)testDeletingMessages {NSMutableArray *messages = [NSMutableArray new];NSManagedObjectContext *ctx = [NSManagedObjectContext MR_contextForCurrentThread];[messages addObject:message];}XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __PRETTY_FUNCTION__]];[self.service deleteMessages:messages withResultBlock:^(NSError *error) {[expectation fulfill];}];}8
- (void)testDeletingMessages {NSMutableArray *messages = [NSMutableArray new];NSManagedObjectContext *ctx = [NSManagedObjectContext MR_contextForCurrentThread];[messages addObject:message];}XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __PRETTY_FUNCTION__]];[self.service deleteMessages:messages withResultBlock:^(NSError *error) {[expectation fulfill];}];}9
- (void)testDeletingMessages {NSMutableArray *messages = [NSMutableArray new];NSManagedObjectContext *ctx = [NSManagedObjectContext MR_contextForCurrentThread];[messages addObject:message];}XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __PRETTY_FUNCTION__]];[self.service deleteMessages:messages withResultBlock:^(NSError *error) {[expectation fulfill];}];}10
- (void)testDeletingMessages {NSMutableArray *messages = [NSMutableArray new];NSManagedObjectContext *ctx = [NSManagedObjectContext MR_contextForCurrentThread];[messages addObject:message];}XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __PRETTY_FUNCTION__]];[self.service deleteMessages:messages withResultBlock:^(NSError *error) {[expectation fulfill];}];}11
Зачем нужны чистые тестыКак писать чистые тестыРефакторим тест12
Зачем нужны чистые тестыКак писать чистые тестыРефакторим тест13
14
/**@author Egor TolstoyМетод возвращает закешированные результаты поиска для определеннойпоисковой строки@param searchTerm Поисковая строка@return Результаты поиска*/- (NSArray *)obtainSearchResultsForSearchTerm:(NSString *)searchTerm;15
@implementation PeopleServiceImplementationTests- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}@end16
@implementation PeopleServiceImplementationTests- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}@end17
@implementation PeopleServiceImplementationTests- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}@end18
@implementation PeopleServiceImplementationTests- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}@end19
@implementation PeopleServiceImplementationTests- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}- (void)testThatService {}@end20
/**Метод возвращает закешированные результаты поиска для определеннойпоисковой строки@param searchTerm Поисковая строка@return Результаты поиска*/+...ServiceReturnsCachedSearchResultsForCorrectQuery...ServiceReturnsNilWhenNoResults...ServiceReturnsNilForInvalidCharacters...ServiceInterpretsDashesAsUnderscores21
Грязные тесты > Тяжело поддерживать >Удаление тестов > Падает качество проекта22
Грязные тесты > Тяжело поддерживать >Удаление тестов > Падает качество проекта23
Грязные тесты > Тяжело поддерживать >Удаление тестов > Падает качество проекта24
Грязные тесты > Тяжело поддерживать >Удаление тестов > Падает качество проекта25
Зачем нужны чистые тестыКак писать чистые тестыРефакторим тест26
Чистый тестпредметно-ориентированный языкбез лишнего контекстатестируется одно поведение системы27
Предметно-ориентированный языкХорошоXCTAssertEqualObjects(testAlbumError, expectedError);- (void)testThatServiceReturnsNilWhenNoResults[self setupStateWithBlockedUser];ПлохоXCTAssertEqualObjects(err1, err2);- (void)testNil[self setupTestData];28
Нет лишнего контекстаХорошо[self stubServiceCompletionBlockWithError:error];- (void)setUp {}Плохо...[invocation getArgument:&result atIndex:3];...... self.interactor.output = OCMProtocolMock(...);...29
Тестируем одно поведениеХорошо...XCTAssertTrue(viewReloaded);XCTAssertTrue(newDataIsShown);Плохо...XCTAssertTrue(newUserSaved);XCTAssertFalse(viewReloaded);XCTAssertNil([self.service obtainSearchHistory]);30
OCMExpect([self.mockView setupInitialStateWithMenuItems:[OCMArg checkWithBlock:^BOOL(NSArray*menuItems) {__block BOOL correctSelectors = YES;[menuItems enumerateObjectsUsingBlock:^(ItemViewModel *menuItem, NSUInteger idx, BOOL stop) {NSString *expectedSelector = selectors[idx];if (![expectedSelector isEqualToString:NSStringFromSelector(menuItem.tapSelector)]) {correctSelectors = NO;}}];return correctSelectors && menuItems.count == selectors.count;}]]);31
OCMExpect([self.mockView setupInitialStateWithMenuItems:[OCMArg checkWithBlock:^BOOL(NSArray*menuItems) {__block BOOL correctSelectors = YES;[menuItems enumerateObjectsUsingBlock:^(ItemViewModel *menuItem, NSUInteger idx, BOOL stop) {NSString *expectedSelector = selectors[idx];if (![expectedSelector isEqualToString:NSStringFromSelector(menuItem.tapSelector)]) {correctSelectors = NO;}}];return correctSelectors && menuItems.count == selectors.count;}]]);self.mockView = [MockMenuView new];XCTAssertTrue(self.mockView.areAllSelectorsCorrect);32
// Случайная строкаNSString *string = [[NSUUID UUID] UUIDString];// Произвольная ошибкаNSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil];33
// Случайная строкаNSString *string = [[NSUUID UUID] UUIDString];// Произвольная ошибкаNSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil];NSString *string = [MockGenerator generateMockString];NSError *error = [MockGenerator generateMockError];34
- (void)setUp {[super setUp];RamblerInitialAssemblyCollector *collector = [RamblerInitialAssemblyCollector new];NSArray *assemblyClasses = [collector collectInitialAssemblyClasses];NSMutableArray *collaboratingAssemblies = [NSMutableArray array];for (Class assemblyClass in assemblyClasses) {if (assemblyClass == [NetworkAssembly class]) {continue;}TyphoonAssembly *assembly = [assemblyClass new];[collaboratingAssemblies addObject:assembly];}NetworkAssembly *networkAssembly = [NetworkAssembly new];[networkAssembly activateWithCollaboratingAssemblies:collaboratingAssemblies];[networkAssembly inject:self];}35
- (void)setUp {[super setUp];RamblerInitialAssemblyCollector *collector = [RamblerInitialAssemblyCollector new];NSArray *assemblyClasses = [collector collectInitialAssemblyClasses];NSMutableArray *collaboratingAssemblies = [NSMutableArray array];for (Class assemblyClass in assemblyClasses) {if (assemblyClass == [NetworkAssembly class]) {continue;}TyphoonAssembly *assembly = [assemblyClass new];[collaboratingAssemblies addObject:assembly];}NetworkAssembly *networkAssembly = [NetworkAssembly new];[networkAssembly activateWithCollaboratingAssemblies:collaboratingAssemblies];[networkAssembly inject:self];[MagicalRecord setupInMemoryCoreData];}- (void)setUp {[self setUpWithAssemblyClass:[NetworkAssembly class]];}36
- (void)testThatServiceLoadsSessionProfileSuccessfully {NSError *resultError;// большой блок логики загрузки профиляXCTAssertNil(resultError);}- (void)testThatServiceLoadsSessionProfileWithError {NSError *expectedError = [MockObjectsFactory generateGeneralError];// большой блок логики загрузки профиляXCTAssertEqualObjects(resultError, expectedError);}37
- (void)testThatServiceLoadsSessionProfileSuccessfully {NSError *resultError;// большой блок логики загрузки профиляXCTAssertNil(resultError);}- (void)testThatServiceLoadsSessionProfileWithError {NSError *expectedError = [MockObjectsFactory generateGeneralError];// большой блок логики загрузки профиляXCTAssertEqualObjects(resultError, expectedError);}- (void)testThatServiceLoadsProfileSuccessfully {[self verifyThatServiceLoadsProfileWithError:nil];}- (void)testThatServiceLoadsProfileWithError {[self verifyThatServiceLoadsProfileWithError:error];}- (void)verifyThatServiceLoadsProfileWithError:(id)error {...}38
- (void)testThatPresenterStartsObservePost {NSString *postId = [MockObjectsFactory generateGeneralString];[self.presenter configureCurrentModuleWithPostId:postId];[self.presenter didTriggerViewReadyEvent];OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]);}39
- (void)testThatPresenterStartsObservePost {NSString *postId = [MockObjectsFactory generateGeneralString];[self.presenter configureCurrentModuleWithPostId:postId];[self.presenter didTriggerViewReadyEvent];OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]);}- (void)testThatPresenterStartsObservePost {// givenNSString *postId = [MockObjectsFactory generateGeneralString];[self.presenter configureCurrentModuleWithPostId:postId];// when[self.presenter didTriggerViewReadyEvent];// thenOCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]);}40
Зачем нужны чистые тестыКак писать чистые тестыРефакторим тест41
OperationSchedulerqueue1 queue2NSOperation NSOperation42
NSArray *operations = self.generalQueue.operations;for (NSOperation *generalOperation in operations) {[generalOperation addDependency:operation];}[self.authQueue addOperation:operation];43
• Передаем initialOperation в Планировщик• Передаем в Планировщик 5 generalOperation• При выполнении initialOperation создает authOperationinitial > authorization > general (5x)44
- (void)testThatAuthOperationBlocksGeneralOperations {// givenXCTestExpectation *expectation = [self expectationForCurrentTest];NSMutableArray *operationNames = [NSMutableArray array];NSString *const kAuthOperationName = @"AuthOperation";NSString *const kInitialOperationName = @"InitialOperation";NSString *const kGeneralOperationName = @"GeneralOperation";NSUInteger const kGeneralOperationsCount = 5;__block NSNumber *operationCounter = @0;NSBlockOperation *authOperation = [NSBlockOperation blockOperationWithBlock:^{dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(operationNames) {[operationNames addObject:kAuthOperationName];}[NSThread sleepForTimeInterval:0.05];});}];NSBlockOperation *initialOperation = [NSBlockOperation blockOperationWithBlock:^{@synchronized(operationNames) {[operationNames addObject:kInitialOperationName];}[self.scheduler addAuthOperation:authOperation];}];// when[self.scheduler addGeneralOperation:initialOperation];for (NSUInteger i = 0; i < kGeneralOperationsCount; i++) {NSBlockOperation *generalOperation = [NSBlockOperation blockOperationWithBlock:^{@synchronized(operationNames) {[operationNames addObject:kGeneralOperationName];}@synchronized(operationCounter) {operationCounter = @([operationCounter integerValue] + 1);if ([operationCounter integerValue] == kGeneralOperationsCount) {dispatch_async(dispatch_get_main_queue(), ^{[expectation fulfill];});}}}];[self.scheduler addGeneralOperation:generalOperation];}// then[self waitForExpectationsWithTimeout:kTestExpectationTimeout handler:^(NSError *error) {XCTAssertEqualObjects(operationNames[0], kInitialOperationName);XCTAssertEqualObjects(operationNames[1], kAuthOperationName);for (NSUInteger i = 2; i < kGeneralOperationsCount; i++) {XCTAssertEqualObjects(operationNames[i], kGeneralOperationName);}}];}45
XCTestExpectation *expectation =[self expectationWithDescription:@"Last operation fired"];XCTestExpectation *expectation =[self expectationForCurrentTest];46
NSString *const kAuthOperationName = @"AuthOperation";NSString *const kInitialOperationName = @"InitialOperation";NSString *const kGeneralOperationName = @"GeneralOperation";OperationSchedulerTestConstants.h47
NSBlockOperation *authOperation = [NSBlockOperation withBlock:^{dispatch_async(..., ^{@synchronized(operationNames) {[operationNames addObject:authName];}[NSThread sleep:0.05];});}];48
@interface TestBlockingByAuthOperationEnvironment : NSObject- (void)setupWithTestCase:(XCTestCase *)testCaseoperationsCount:(NSUInteger)operationsCountinitialBlock:(Block)initialBlock;@property NSBlockOperation *initialOperation;@property NSBlockOperation *authOperation;@property NSArray *generalOperations;@property NSArray *firedOperationNames;@end49
TestBlockingByAuthOperationEnvironment *environment =[TestBlockingByAuthOperationEnvironment new];[environment setupWithTestCase:selfoperationsCount:kGeneralOperationsCountinitialBlock:^{[self.scheduler addAuthOperation:environment.authOperation];for (NSOperation *operation in environment.generalOperations) {[self.scheduler addGeneralOperation:operation];}}];50
- (void)testThatAuthOperationBlocksGeneralOperations {// givenNSUInteger const kGeneralOperationsCount = 5;TestBlockingByAuthOperationEnvironment *environment = [TestBlockingByAuthOperationEnvironment new];[environment setupEnvironmentWithTestCase:selfgeneralOperationsCount:kGeneralOperationsCountinitialOperationBlock:^{[self.scheduler addAuthOperation:environment.authOperation];for (NSOperation *operation in environment.generalOperations) {[self.scheduler addGeneralOperation:operation];}}];// when[self.scheduler addGeneralOperation:environment.initialOperation];// then[self waitForExpectationsWithTimeout:kTestExpectationTimeout handler:^(NSError *error) {[self verifyCorrectOperationOrder:kTestOrder];}];}51
Предметно-ориентированный языкНет лишнего контекстаТестируем одно поведение52
What makes a clean test?Three things.Readability, readability,and readability.Егор Толстой@igrekde