Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Clean Unit Tests

Clean Unit Tests

The story about why unit tests matter.

Faf4a633b0d1e32a07a071039686c868?s=128

Egor Tolstoy

October 05, 2016
Tweet

Transcript

  1. Чистые unit-тесты

  2. What makes a clean test? Three things. Readability, readability, and

    readability. Robert C. Martin, «Clean Code»
  3. #добришко

  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]); }]; } 4
  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]); }]; } 5
  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]); }]; } 6
  7. - (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
  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]; }]; } 8
  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]; }]; } 9
  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]; }]; } 10
  11. - (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. Зачем нужны чистые тесты Как писать чистые тесты Рефакторим тест

    12
  13. Зачем нужны чистые тесты Как писать чистые тесты Рефакторим тест

    13
  14. 14

  15. /** @author Egor Tolstoy Метод возвращает закешированные результаты поиска для

    определенной поисковой строки @param searchTerm Поисковая строка @return Результаты поиска */ - (NSArray *)obtainSearchResultsForSearchTerm:(NSString *)searchTerm; 15
  16. @implementation PeopleServiceImplementationTests - (void)testThatService { } - (void)testThatService { }

    - (void)testThatService { } - (void)testThatService { } @end 16
  17. @implementation PeopleServiceImplementationTests - (void)testThatService { } - (void)testThatService { }

    - (void)testThatService { } - (void)testThatService { } @end 17
  18. @implementation PeopleServiceImplementationTests - (void)testThatService { } - (void)testThatService { }

    - (void)testThatService { } - (void)testThatService { } @end 18
  19. @implementation PeopleServiceImplementationTests - (void)testThatService { } - (void)testThatService { }

    - (void)testThatService { } - (void)testThatService { } @end 19
  20. @implementation PeopleServiceImplementationTests - (void)testThatService { } - (void)testThatService { }

    - (void)testThatService { } - (void)testThatService { } @end 20
  21. /** Метод возвращает закешированные результаты поиска для определенной поисковой строки

    @param searchTerm Поисковая строка @return Результаты поиска */ + ...ServiceReturnsCachedSearchResultsForCorrectQuery ...ServiceReturnsNilWhenNoResults ...ServiceReturnsNilForInvalidCharacters ...ServiceInterpretsDashesAsUnderscores 21
  22. Грязные тесты > Тяжело поддерживать > Удаление тестов > Падает

    качество проекта 22
  23. Грязные тесты > Тяжело поддерживать > Удаление тестов > Падает

    качество проекта 23
  24. Грязные тесты > Тяжело поддерживать > Удаление тестов > Падает

    качество проекта 24
  25. Грязные тесты > Тяжело поддерживать > Удаление тестов > Падает

    качество проекта 25
  26. Зачем нужны чистые тесты Как писать чистые тесты Рефакторим тест

    26
  27. Чистый тест предметно-ориентированный язык без лишнего контекста тестируется одно поведение

    системы 27
  28. Предметно-ориентированный язык Хорошо XCTAssertEqualObjects(testAlbumError, expectedError); - (void)testThatServiceReturnsNilWhenNoResults [self setupStateWithBlockedUser]; Плохо

    XCTAssertEqualObjects(err1, err2); - (void)testNil [self setupTestData]; 28
  29. Нет лишнего контекста Хорошо [self stubServiceCompletionBlockWithError:error]; - (void)setUp {} Плохо

    ...[invocation getArgument:&result atIndex:3];... ... self.interactor.output = OCMProtocolMock(...);... 29
  30. Тестируем одно поведение Хорошо ... XCTAssertTrue(viewReloaded); XCTAssertTrue(newDataIsShown); Плохо ... XCTAssertTrue(newUserSaved);

    XCTAssertFalse(viewReloaded); XCTAssertNil([self.service obtainSearchHistory]); 30
  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; }]]); 31
  32. 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
  33. // Случайная строка NSString *string = [[NSUUID UUID] UUIDString]; //

    Произвольная ошибка NSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil]; 33
  34. // Случайная строка NSString *string = [[NSUUID UUID] UUIDString]; //

    Произвольная ошибка NSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil]; NSString *string = [MockGenerator generateMockString]; NSError *error = [MockGenerator generateMockError]; 34
  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]; } 35
  36. - (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
  37. - (void)testThatServiceLoadsSessionProfileSuccessfully { NSError *resultError; // большой блок логики загрузки

    профиля XCTAssertNil(resultError); } - (void)testThatServiceLoadsSessionProfileWithError { NSError *expectedError = [MockObjectsFactory generateGeneralError]; // большой блок логики загрузки профиля XCTAssertEqualObjects(resultError, expectedError); } 37
  38. - (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
  39. - (void)testThatPresenterStartsObservePost { NSString *postId = [MockObjectsFactory generateGeneralString]; [self.presenter configureCurrentModuleWithPostId:postId];

    [self.presenter didTriggerViewReadyEvent]; OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]); } 39
  40. - (void)testThatPresenterStartsObservePost { NSString *postId = [MockObjectsFactory generateGeneralString]; [self.presenter configureCurrentModuleWithPostId:postId];

    [self.presenter didTriggerViewReadyEvent]; OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]); } - (void)testThatPresenterStartsObservePost { // given NSString *postId = [MockObjectsFactory generateGeneralString]; [self.presenter configureCurrentModuleWithPostId:postId]; // when [self.presenter didTriggerViewReadyEvent]; // then OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]); } 40
  41. Зачем нужны чистые тесты Как писать чистые тесты Рефакторим тест

    41
  42. OperationScheduler queue1 queue2 NSOperation NSOperation 42

  43. NSArray *operations = self.generalQueue.operations; for (NSOperation *generalOperation in operations) {

    [generalOperation addDependency:operation]; } [self.authQueue addOperation:operation]; 43
  44. • Передаем initialOperation в Планировщик • Передаем в Планировщик 5

    generalOperation • При выполнении initialOperation создает authOperation initial > authorization > general (5x) 44
  45. - (void)testThatAuthOperationBlocksGeneralOperations { // given XCTestExpectation *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
  46. XCTestExpectation *expectation = [self expectationWithDescription:@"Last operation fired"]; XCTestExpectation *expectation =

    [self expectationForCurrentTest]; 46
  47. NSString *const kAuthOperationName = @"AuthOperation"; NSString *const kInitialOperationName = @"InitialOperation";

    NSString *const kGeneralOperationName = @"GeneralOperation"; OperationSchedulerTestConstants.h 47
  48. NSBlockOperation *authOperation = [NSBlockOperation withBlock:^{ dispatch_async(..., ^{ @synchronized(operationNames) { [operationNames

    addObject:authName]; } [NSThread sleep:0.05]; }); }]; 48
  49. @interface TestBlockingByAuthOperationEnvironment : NSObject - (void)setupWithTestCase:(XCTestCase *)testCase operationsCount:(NSUInteger)operationsCount initialBlock:(Block)initialBlock; @property

    NSBlockOperation *initialOperation; @property NSBlockOperation *authOperation; @property NSArray *generalOperations; @property NSArray *firedOperationNames; @end 49
  50. TestBlockingByAuthOperationEnvironment *environment = [TestBlockingByAuthOperationEnvironment new]; [environment setupWithTestCase:self operationsCount:kGeneralOperationsCount initialBlock:^{ [self.scheduler

    addAuthOperation:environment.authOperation]; for (NSOperation *operation in environment.generalOperations) { [self.scheduler addGeneralOperation:operation]; } }]; 50
  51. - (void)testThatAuthOperationBlocksGeneralOperations { // given NSUInteger const kGeneralOperationsCount = 5;

    TestBlockingByAuthOperationEnvironment *environment = [TestBlockingByAuthOperationEnvironment new]; [environment setupEnvironmentWithTestCase:self generalOperationsCount:kGeneralOperationsCount initialOperationBlock:^{ [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. Предметно-ориентированный язык Нет лишнего контекста Тестируем одно поведение 52

  53. What makes a clean test? Three things. Readability, readability, and

    readability. Егор Толстой @igrekde