Чистые
unit-тесты
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 {
}
@end
16
@implementation PeopleServiceImplementationTests
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
@end
17
@implementation PeopleServiceImplementationTests
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
@end
18
@implementation PeopleServiceImplementationTests
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
@end
19
@implementation PeopleServiceImplementationTests
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
- (void)testThatService {
}
@end
20
/**
Метод возвращает закешированные результаты поиска для определенной
поисковой строки
@param searchTerm Поисковая строка
@return Результаты поиска
*/
+
...ServiceReturnsCachedSearchResultsForCorrectQuery
...ServiceReturnsNilWhenNoResults
...ServiceReturnsNilForInvalidCharacters
...ServiceInterpretsDashesAsUnderscores
21
Грязные тесты > Тяжело поддерживать >
Удаление тестов > Падает качество проекта
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 {
// given
NSString *postId = [MockObjectsFactory generateGeneralString];
[self.presenter configureCurrentModuleWithPostId:postId];
// when
[self.presenter didTriggerViewReadyEvent];
// then
OCMVerify([self.mockInteractor startObserveChangesWithPostId:postId]);
}
40
Зачем нужны чистые тесты
Как писать чистые тесты
Рефакторим тест
41
OperationScheduler
queue1 queue2
NSOperation NSOperation
42
NSArray *operations = self.generalQueue.operations;
for (NSOperation *generalOperation in operations) {
[generalOperation addDependency:operation];
}
[self.authQueue addOperation:operation];
43
• Передаем initialOperation в Планировщик
• Передаем в Планировщик 5 generalOperation
• При выполнении initialOperation создает authOperation
initial > authorization > general (5x)
44
- (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
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.h
47
NSBlockOperation *authOperation = [NSBlockOperation withBlock:^{
dispatch_async(..., ^{
@synchronized(operationNames) {
[operationNames addObject:authName];
}
[NSThread sleep:0.05];
});
}];
48
@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
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
- (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
What makes a clean test?
Three things.
Readability, readability,
and readability.
Егор Толстой
@igrekde

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

  • 1.
  • 2.
    What makes aclean 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.
  • 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.
    Нет лишнего контекста Хорошо [selfstubServiceCompletionBlockWithError:error]; - (void)setUp {} Плохо ...[invocation getArgument:&result atIndex:3];... ... self.interactor.output = OCMProtocolMock(...);... 29
  • 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 { [supersetUp]; 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 { [supersetUp]; 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.
  • 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 = [selfexpectationWithDescription:@"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 = [TestBlockingByAuthOperationEnvironmentnew]; [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 aclean test? Three things. Readability, readability, and readability. Егор Толстой @igrekde