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.

Egor Tolstoy

October 05, 2016
Tweet

More Decks by Egor Tolstoy

Other Decks in Technology

Transcript

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

    View Slide

  2. What makes a clean test?
    Three things. Readability, readability, and readability.
    Robert C. Martin, «Clean Code»

    View Slide

  3. #добришко

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  14. 14

    View Slide

  15. /**
    @author Egor Tolstoy
    Метод возвращает закешированные результаты поиска для определенной
    поисковой строки
    @param searchTerm Поисковая строка
    @return Результаты поиска
    */
    - (NSArray *)obtainSearchResultsForSearchTerm:(NSString *)searchTerm;
    15

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. /**
    Метод возвращает закешированные результаты поиска для определенной
    поисковой строки
    @param searchTerm Поисковая строка
    @return Результаты поиска
    */
    +
    ...ServiceReturnsCachedSearchResultsForCorrectQuery
    ...ServiceReturnsNilWhenNoResults
    ...ServiceReturnsNilForInvalidCharacters
    ...ServiceInterpretsDashesAsUnderscores
    21

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. Нет лишнего контекста
    Хорошо
    [self stubServiceCompletionBlockWithError:error];
    - (void)setUp {}
    Плохо
    ...[invocation getArgument:&result atIndex:3];...
    ... self.interactor.output = OCMProtocolMock(...);...
    29

    View Slide

  30. Тестируем одно поведение
    Хорошо
    ...
    XCTAssertTrue(viewReloaded);
    XCTAssertTrue(newDataIsShown);
    Плохо
    ...
    XCTAssertTrue(newUserSaved);
    XCTAssertFalse(viewReloaded);
    XCTAssertNil([self.service obtainSearchHistory]);
    30

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  37. - (void)testThatServiceLoadsSessionProfileSuccessfully {
    NSError *resultError;
    // большой блок логики загрузки профиля
    XCTAssertNil(resultError);
    }
    - (void)testThatServiceLoadsSessionProfileWithError {
    NSError *expectedError = [MockObjectsFactory generateGeneralError];
    // большой блок логики загрузки профиля
    XCTAssertEqualObjects(resultError, expectedError);
    }
    37

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  42. OperationScheduler
    queue1 queue2
    NSOperation NSOperation
    42

    View Slide

  43. NSArray *operations = self.generalQueue.operations;
    for (NSOperation *generalOperation in operations) {
    [generalOperation addDependency:operation];
    }
    [self.authQueue addOperation:operation];
    43

    View Slide

  44. • Передаем initialOperation в Планировщик
    • Передаем в Планировщик 5 generalOperation
    • При выполнении initialOperation создает authOperation
    initial > authorization > general (5x)
    44

    View Slide

  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

    View Slide

  46. XCTestExpectation *expectation =
    [self expectationWithDescription:@"Last operation fired"];
    XCTestExpectation *expectation =
    [self expectationForCurrentTest];
    46

    View Slide

  47. NSString *const kAuthOperationName = @"AuthOperation";
    NSString *const kInitialOperationName = @"InitialOperation";
    NSString *const kGeneralOperationName = @"GeneralOperation";
    OperationSchedulerTestConstants.h
    47

    View Slide

  48. NSBlockOperation *authOperation = [NSBlockOperation withBlock:^{
    dispatch_async(..., ^{
    @synchronized(operationNames) {
    [operationNames addObject:authName];
    }
    [NSThread sleep:0.05];
    });
    }];
    48

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  52. Предметно-ориентированный язык
    Нет лишнего контекста
    Тестируем одно поведение
    52

    View Slide

  53. What makes a clean test?
    Three things.
    Readability, readability,
    and readability.
    Егор Толстой
    @igrekde

    View Slide