53
Чистые unit-тесты

Rambler.iOS #8 - Чистые unit-тесты

Embed Size (px)

Citation preview

Page 1: Rambler.iOS #8 - Чистые unit-тесты

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

Page 2: Rambler.iOS #8 - Чистые unit-тесты

What makes a clean test?

Three things. Readability, readability, and readability.

Robert C. Martin, «Clean Code»

Page 3: Rambler.iOS #8 - Чистые unit-тесты

#добришко

Page 4: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 5: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 6: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 7: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 8: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 9: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 10: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 11: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 12: Rambler.iOS #8 - Чистые unit-тесты

Зачем нужны чистые тесты

Как писать чистые тесты

Рефакторим тест

12

Page 13: Rambler.iOS #8 - Чистые unit-тесты

Зачем нужны чистые тесты

Как писать чистые тесты

Рефакторим тест

13

Page 14: Rambler.iOS #8 - Чистые unit-тесты

14

Page 15: Rambler.iOS #8 - Чистые unit-тесты

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

15

Page 16: Rambler.iOS #8 - Чистые unit-тесты

@implementation PeopleServiceImplementationTests

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

@end

16

Page 17: Rambler.iOS #8 - Чистые unit-тесты

@implementation PeopleServiceImplementationTests

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

@end

17

Page 18: Rambler.iOS #8 - Чистые unit-тесты

@implementation PeopleServiceImplementationTests

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

@end

18

Page 19: Rambler.iOS #8 - Чистые unit-тесты

@implementation PeopleServiceImplementationTests

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

@end

19

Page 20: Rambler.iOS #8 - Чистые unit-тесты

@implementation PeopleServiceImplementationTests

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

- (void)testThatService {}

@end

20

Page 21: Rambler.iOS #8 - Чистые unit-тесты

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

+ ...ServiceReturnsCachedSearchResultsForCorrectQuery ...ServiceReturnsNilWhenNoResults ...ServiceReturnsNilForInvalidCharacters ...ServiceInterpretsDashesAsUnderscores

21

Page 22: Rambler.iOS #8 - Чистые unit-тесты

Грязные тесты > Тяжело поддерживать >

Удаление тестов > Падает качество проекта

22

Page 23: Rambler.iOS #8 - Чистые unit-тесты

Грязные тесты > Тяжело поддерживать >

Удаление тестов > Падает качество проекта

23

Page 24: Rambler.iOS #8 - Чистые unit-тесты

Грязные тесты > Тяжело поддерживать >

Удаление тестов > Падает качество проекта

24

Page 25: Rambler.iOS #8 - Чистые unit-тесты

Грязные тесты > Тяжело поддерживать >

Удаление тестов > Падает качество проекта

25

Page 26: Rambler.iOS #8 - Чистые unit-тесты

Зачем нужны чистые тесты

Как писать чистые тесты

Рефакторим тест

26

Page 27: Rambler.iOS #8 - Чистые unit-тесты

Чистый тест

предметно-ориентированный язык

без лишнего контекста

тестируется одно поведение системы

27

Page 28: Rambler.iOS #8 - Чистые unit-тесты

Предметно-ориентированный язык

Хорошо XCTAssertEqualObjects(testAlbumError, expectedError); - (void)testThatServiceReturnsNilWhenNoResults [self setupStateWithBlockedUser];

Плохо XCTAssertEqualObjects(err1, err2); - (void)testNil [self setupTestData];

28

Page 29: Rambler.iOS #8 - Чистые unit-тесты

Нет лишнего контекста

Хорошо [self stubServiceCompletionBlockWithError:error]; - (void)setUp {}

Плохо ...[invocation getArgument:&result atIndex:3];...

... self.interactor.output = OCMProtocolMock(...);...

29

Page 30: Rambler.iOS #8 - Чистые unit-тесты

Тестируем одно поведениеХорошо ... XCTAssertTrue(viewReloaded); XCTAssertTrue(newDataIsShown);

Плохо ... XCTAssertTrue(newUserSaved); XCTAssertFalse(viewReloaded); XCTAssertNil([self.service obtainSearchHistory]);

30

Page 31: Rambler.iOS #8 - Чистые unit-тесты

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

Page 32: Rambler.iOS #8 - Чистые unit-тесты

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

Page 33: Rambler.iOS #8 - Чистые unit-тесты

// Случайная строка NSString *string = [[NSUUID UUID] UUIDString];

// Произвольная ошибка NSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil];

33

Page 34: Rambler.iOS #8 - Чистые unit-тесты

// Случайная строка NSString *string = [[NSUUID UUID] UUIDString];

// Произвольная ошибка NSError *error = [NSError errorWithDomain:@"TestDomain" code:0 userInfo:nil];

NSString *string = [MockGenerator generateMockString]; NSError *error = [MockGenerator generateMockError];

34

Page 35: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 36: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 37: Rambler.iOS #8 - Чистые unit-тесты

- (void)testThatServiceLoadsSessionProfileSuccessfully { NSError *resultError;

// большой блок логики загрузки профиля XCTAssertNil(resultError); }

- (void)testThatServiceLoadsSessionProfileWithError { NSError *expectedError = [MockObjectsFactory generateGeneralError];

// большой блок логики загрузки профиля XCTAssertEqualObjects(resultError, expectedError); }

37

Page 38: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 39: Rambler.iOS #8 - Чистые unit-тесты

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

39

Page 40: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 41: Rambler.iOS #8 - Чистые unit-тесты

Зачем нужны чистые тесты

Как писать чистые тесты

Рефакторим тест

41

Page 42: Rambler.iOS #8 - Чистые unit-тесты

OperationScheduler

queue1 queue2

NSOperation NSOperation42

Page 43: Rambler.iOS #8 - Чистые unit-тесты

NSArray *operations = self.generalQueue.operations;

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

43

Page 44: Rambler.iOS #8 - Чистые unit-тесты

• Передаем initialOperation в Планировщик

• Передаем в Планировщик 5 generalOperation

• При выполнении initialOperation создает authOperation

initial > authorization > general (5x)

44

Page 45: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 46: Rambler.iOS #8 - Чистые unit-тесты

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

XCTestExpectation *expectation = [self expectationForCurrentTest];

46

Page 47: Rambler.iOS #8 - Чистые unit-тесты

NSString *const kAuthOperationName = @"AuthOperation"; NSString *const kInitialOperationName = @"InitialOperation"; NSString *const kGeneralOperationName = @"GeneralOperation";

OperationSchedulerTestConstants.h

47

Page 48: Rambler.iOS #8 - Чистые unit-тесты

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

48

Page 49: Rambler.iOS #8 - Чистые unit-тесты

@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

Page 50: Rambler.iOS #8 - Чистые unit-тесты

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

Page 51: Rambler.iOS #8 - Чистые unit-тесты

- (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

Page 52: Rambler.iOS #8 - Чистые unit-тесты

Предметно-ориентированный язык

Нет лишнего контекста

Тестируем одно поведение

52

Page 53: Rambler.iOS #8 - Чистые unit-тесты

What makes a clean test?

Three things.

Readability, readability,

and readability.

Егор Толстой

@igrekde