SlideShare a Scribd company logo
1 of 53
Download to read offline
Чистые
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

More Related Content

What's hot

JUG.ua 20170225 - Java bytecode instrumentation
JUG.ua 20170225 - Java bytecode instrumentationJUG.ua 20170225 - Java bytecode instrumentation
JUG.ua 20170225 - Java bytecode instrumentationAnton Arhipov
 
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014RxJava, Getting Started - David Wursteisen - 16 Octobre 2014
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014SOAT
 
C++ Programming - 13th Study
C++ Programming - 13th StudyC++ Programming - 13th Study
C++ Programming - 13th StudyChris Ohk
 
Collection pipeline par Mathieu Godart
Collection pipeline par  Mathieu GodartCollection pipeline par  Mathieu Godart
Collection pipeline par Mathieu GodartCocoaHeads France
 
Lightning talk second
Lightning talk secondLightning talk second
Lightning talk secondShinUsuda
 
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsOpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsHo Kim
 
Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Wilson Su
 
JavaScript Assíncrono
JavaScript AssíncronoJavaScript Assíncrono
JavaScript AssíncronoNatã Barbosa
 
Java Thread Cronometro
Java Thread CronometroJava Thread Cronometro
Java Thread Cronometrojubacalo
 
Node.JS
Node.JSNode.JS
Node.JSeibaan
 
Testes unitários de JS com Jasmine e Karma
Testes unitários de JS com Jasmine e KarmaTestes unitários de JS com Jasmine e Karma
Testes unitários de JS com Jasmine e KarmaDouglas Matoso
 
D2D Pizza JS Игорь Ковган "Koa поможет"
D2D Pizza JS Игорь Ковган "Koa поможет"D2D Pizza JS Игорь Ковган "Koa поможет"
D2D Pizza JS Игорь Ковган "Koa поможет"Dev2Dev
 
Rafael torrest
Rafael torrestRafael torrest
Rafael torrestrfltorres
 
Clang-tidy: путешествие внутрь AST C++
Clang-tidy: путешествие внутрь AST C++Clang-tidy: путешествие внутрь AST C++
Clang-tidy: путешествие внутрь AST C++corehard_by
 
Ejb 3.0 Glassfish 2.X Netbeans 6.X
Ejb 3.0 Glassfish 2.X Netbeans 6.XEjb 3.0 Glassfish 2.X Netbeans 6.X
Ejb 3.0 Glassfish 2.X Netbeans 6.Xa19987225
 
Tugas pemrograman jaringan
Tugas pemrograman jaringanTugas pemrograman jaringan
Tugas pemrograman jaringanBanser Sahara
 

What's hot (20)

JUG.ua 20170225 - Java bytecode instrumentation
JUG.ua 20170225 - Java bytecode instrumentationJUG.ua 20170225 - Java bytecode instrumentation
JUG.ua 20170225 - Java bytecode instrumentation
 
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014RxJava, Getting Started - David Wursteisen - 16 Octobre 2014
RxJava, Getting Started - David Wursteisen - 16 Octobre 2014
 
Testování prakticky
Testování praktickyTestování prakticky
Testování prakticky
 
C++ Programming - 13th Study
C++ Programming - 13th StudyC++ Programming - 13th Study
C++ Programming - 13th Study
 
Collection pipeline par Mathieu Godart
Collection pipeline par  Mathieu GodartCollection pipeline par  Mathieu Godart
Collection pipeline par Mathieu Godart
 
Lightning talk second
Lightning talk secondLightning talk second
Lightning talk second
 
Kruskal algorithm
Kruskal algorithmKruskal algorithm
Kruskal algorithm
 
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsOpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
 
Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8
 
JavaScript Assíncrono
JavaScript AssíncronoJavaScript Assíncrono
JavaScript Assíncrono
 
Java Thread Cronometro
Java Thread CronometroJava Thread Cronometro
Java Thread Cronometro
 
Sockets java
Sockets javaSockets java
Sockets java
 
Mikstura it2013
Mikstura it2013Mikstura it2013
Mikstura it2013
 
Node.JS
Node.JSNode.JS
Node.JS
 
Testes unitários de JS com Jasmine e Karma
Testes unitários de JS com Jasmine e KarmaTestes unitários de JS com Jasmine e Karma
Testes unitários de JS com Jasmine e Karma
 
D2D Pizza JS Игорь Ковган "Koa поможет"
D2D Pizza JS Игорь Ковган "Koa поможет"D2D Pizza JS Игорь Ковган "Koa поможет"
D2D Pizza JS Игорь Ковган "Koa поможет"
 
Rafael torrest
Rafael torrestRafael torrest
Rafael torrest
 
Clang-tidy: путешествие внутрь AST C++
Clang-tidy: путешествие внутрь AST C++Clang-tidy: путешествие внутрь AST C++
Clang-tidy: путешествие внутрь AST C++
 
Ejb 3.0 Glassfish 2.X Netbeans 6.X
Ejb 3.0 Glassfish 2.X Netbeans 6.XEjb 3.0 Glassfish 2.X Netbeans 6.X
Ejb 3.0 Glassfish 2.X Netbeans 6.X
 
Tugas pemrograman jaringan
Tugas pemrograman jaringanTugas pemrograman jaringan
Tugas pemrograman jaringan
 

Viewers also liked

2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS
2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS
2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOSОмские ИТ-субботники
 
Rambler.iOS #5: Переходы и передача данных между VIPER модулями
Rambler.iOS #5: Переходы и передача данных между VIPER модулямиRambler.iOS #5: Переходы и передача данных между VIPER модулями
Rambler.iOS #5: Переходы и передача данных между VIPER модулямиRAMBLER&Co
 
Rambler.iOS #5: VIPER a la Rambler
Rambler.iOS #5: VIPER a la RamblerRambler.iOS #5: VIPER a la Rambler
Rambler.iOS #5: VIPER a la RamblerRAMBLER&Co
 
Rambler.iOS #5: Подмодули в VIPER
Rambler.iOS #5: Подмодули в VIPERRambler.iOS #5: Подмодули в VIPER
Rambler.iOS #5: Подмодули в VIPERRAMBLER&Co
 
Rambler.iOS #5: VIPER и Swift
Rambler.iOS #5: VIPER и SwiftRambler.iOS #5: VIPER и Swift
Rambler.iOS #5: VIPER и SwiftRAMBLER&Co
 
RDSDataSource: Чистые тесты на Swift
RDSDataSource: Чистые тесты на SwiftRDSDataSource: Чистые тесты на Swift
RDSDataSource: Чистые тесты на SwiftRAMBLER&Co
 

Viewers also liked (6)

2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS
2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS
2016-08-20 02 Антон Ковалев, Антон Кормаков. Viper. Чистая архитектура для iOS
 
Rambler.iOS #5: Переходы и передача данных между VIPER модулями
Rambler.iOS #5: Переходы и передача данных между VIPER модулямиRambler.iOS #5: Переходы и передача данных между VIPER модулями
Rambler.iOS #5: Переходы и передача данных между VIPER модулями
 
Rambler.iOS #5: VIPER a la Rambler
Rambler.iOS #5: VIPER a la RamblerRambler.iOS #5: VIPER a la Rambler
Rambler.iOS #5: VIPER a la Rambler
 
Rambler.iOS #5: Подмодули в VIPER
Rambler.iOS #5: Подмодули в VIPERRambler.iOS #5: Подмодули в VIPER
Rambler.iOS #5: Подмодули в VIPER
 
Rambler.iOS #5: VIPER и Swift
Rambler.iOS #5: VIPER и SwiftRambler.iOS #5: VIPER и Swift
Rambler.iOS #5: VIPER и Swift
 
RDSDataSource: Чистые тесты на Swift
RDSDataSource: Чистые тесты на SwiftRDSDataSource: Чистые тесты на Swift
RDSDataSource: Чистые тесты на Swift
 

More from RAMBLER&Co

RDSDataSource: Основы LLVM
RDSDataSource: Основы LLVMRDSDataSource: Основы LLVM
RDSDataSource: Основы LLVMRAMBLER&Co
 
Rambler.iOS #9: Анализируй это!
Rambler.iOS #9: Анализируй это!Rambler.iOS #9: Анализируй это!
Rambler.iOS #9: Анализируй это!RAMBLER&Co
 
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?RAMBLER&Co
 
Rambler.iOS #9: Life with out of memory
Rambler.iOS #9: Life with out of memoryRambler.iOS #9: Life with out of memory
Rambler.iOS #9: Life with out of memoryRAMBLER&Co
 
RDSDataSource: OCLint
RDSDataSource: OCLintRDSDataSource: OCLint
RDSDataSource: OCLintRAMBLER&Co
 
RDSDataSource: Построение UML диаграмм
RDSDataSource: Построение UML диаграммRDSDataSource: Построение UML диаграмм
RDSDataSource: Построение UML диаграммRAMBLER&Co
 
RDSDataSource: App Thinning
RDSDataSource: App ThinningRDSDataSource: App Thinning
RDSDataSource: App ThinningRAMBLER&Co
 
RDSDataSource: Мастер-класс по Dip
RDSDataSource: Мастер-класс по DipRDSDataSource: Мастер-класс по Dip
RDSDataSource: Мастер-класс по DipRAMBLER&Co
 
RDSDataSource: YapDatabase
RDSDataSource: YapDatabaseRDSDataSource: YapDatabase
RDSDataSource: YapDatabaseRAMBLER&Co
 
Rambler.iOS #8: Сервис-ориентированная архитектура
Rambler.iOS #8: Сервис-ориентированная архитектураRambler.iOS #8: Сервис-ориентированная архитектура
Rambler.iOS #8: Сервис-ориентированная архитектураRAMBLER&Co
 
Rambler.iOS #8: Make your app extensible with JavaScriptCore
Rambler.iOS #8: Make your app extensible with JavaScriptCoreRambler.iOS #8: Make your app extensible with JavaScriptCore
Rambler.iOS #8: Make your app extensible with JavaScriptCoreRAMBLER&Co
 
Rambler.iOS #8: Как не стать жертвой бэкендеров
Rambler.iOS #8: Как не стать жертвой бэкендеровRambler.iOS #8: Как не стать жертвой бэкендеров
Rambler.iOS #8: Как не стать жертвой бэкендеровRAMBLER&Co
 
RDSDataSource: iOS Reverse Engineering for inexperienced
RDSDataSource: iOS Reverse Engineering for inexperiencedRDSDataSource: iOS Reverse Engineering for inexperienced
RDSDataSource: iOS Reverse Engineering for inexperiencedRAMBLER&Co
 
RDSDataSource: Автогенерация документации для SDK
RDSDataSource: Автогенерация документации для SDKRDSDataSource: Автогенерация документации для SDK
RDSDataSource: Автогенерация документации для SDKRAMBLER&Co
 
RDSDataSource: Плюрализация в iOS
RDSDataSource: Плюрализация в iOSRDSDataSource: Плюрализация в iOS
RDSDataSource: Плюрализация в iOSRAMBLER&Co
 
RDSDataSource: Promises
RDSDataSource: PromisesRDSDataSource: Promises
RDSDataSource: PromisesRAMBLER&Co
 
RDSDataSource: Flux, Redux, ReSwift
RDSDataSource: Flux, Redux, ReSwiftRDSDataSource: Flux, Redux, ReSwift
RDSDataSource: Flux, Redux, ReSwiftRAMBLER&Co
 
Rambler.iOS #7: Построение сложного табличного интерфейса
Rambler.iOS #7: Построение сложного табличного интерфейсаRambler.iOS #7: Построение сложного табличного интерфейса
Rambler.iOS #7: Построение сложного табличного интерфейсаRAMBLER&Co
 
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложении
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложенииRambler.iOS #7: Прием платежей по банковским картам в iOS приложении
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложенииRAMBLER&Co
 
Rambler.iOS #7: Интернет-эквайринг 101
Rambler.iOS #7: Интернет-эквайринг 101Rambler.iOS #7: Интернет-эквайринг 101
Rambler.iOS #7: Интернет-эквайринг 101RAMBLER&Co
 

More from RAMBLER&Co (20)

RDSDataSource: Основы LLVM
RDSDataSource: Основы LLVMRDSDataSource: Основы LLVM
RDSDataSource: Основы LLVM
 
Rambler.iOS #9: Анализируй это!
Rambler.iOS #9: Анализируй это!Rambler.iOS #9: Анализируй это!
Rambler.iOS #9: Анализируй это!
 
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?
Rambler.iOS #9: Нужны ли бэкенд-разработчики, когда есть Swift?
 
Rambler.iOS #9: Life with out of memory
Rambler.iOS #9: Life with out of memoryRambler.iOS #9: Life with out of memory
Rambler.iOS #9: Life with out of memory
 
RDSDataSource: OCLint
RDSDataSource: OCLintRDSDataSource: OCLint
RDSDataSource: OCLint
 
RDSDataSource: Построение UML диаграмм
RDSDataSource: Построение UML диаграммRDSDataSource: Построение UML диаграмм
RDSDataSource: Построение UML диаграмм
 
RDSDataSource: App Thinning
RDSDataSource: App ThinningRDSDataSource: App Thinning
RDSDataSource: App Thinning
 
RDSDataSource: Мастер-класс по Dip
RDSDataSource: Мастер-класс по DipRDSDataSource: Мастер-класс по Dip
RDSDataSource: Мастер-класс по Dip
 
RDSDataSource: YapDatabase
RDSDataSource: YapDatabaseRDSDataSource: YapDatabase
RDSDataSource: YapDatabase
 
Rambler.iOS #8: Сервис-ориентированная архитектура
Rambler.iOS #8: Сервис-ориентированная архитектураRambler.iOS #8: Сервис-ориентированная архитектура
Rambler.iOS #8: Сервис-ориентированная архитектура
 
Rambler.iOS #8: Make your app extensible with JavaScriptCore
Rambler.iOS #8: Make your app extensible with JavaScriptCoreRambler.iOS #8: Make your app extensible with JavaScriptCore
Rambler.iOS #8: Make your app extensible with JavaScriptCore
 
Rambler.iOS #8: Как не стать жертвой бэкендеров
Rambler.iOS #8: Как не стать жертвой бэкендеровRambler.iOS #8: Как не стать жертвой бэкендеров
Rambler.iOS #8: Как не стать жертвой бэкендеров
 
RDSDataSource: iOS Reverse Engineering for inexperienced
RDSDataSource: iOS Reverse Engineering for inexperiencedRDSDataSource: iOS Reverse Engineering for inexperienced
RDSDataSource: iOS Reverse Engineering for inexperienced
 
RDSDataSource: Автогенерация документации для SDK
RDSDataSource: Автогенерация документации для SDKRDSDataSource: Автогенерация документации для SDK
RDSDataSource: Автогенерация документации для SDK
 
RDSDataSource: Плюрализация в iOS
RDSDataSource: Плюрализация в iOSRDSDataSource: Плюрализация в iOS
RDSDataSource: Плюрализация в iOS
 
RDSDataSource: Promises
RDSDataSource: PromisesRDSDataSource: Promises
RDSDataSource: Promises
 
RDSDataSource: Flux, Redux, ReSwift
RDSDataSource: Flux, Redux, ReSwiftRDSDataSource: Flux, Redux, ReSwift
RDSDataSource: Flux, Redux, ReSwift
 
Rambler.iOS #7: Построение сложного табличного интерфейса
Rambler.iOS #7: Построение сложного табличного интерфейсаRambler.iOS #7: Построение сложного табличного интерфейса
Rambler.iOS #7: Построение сложного табличного интерфейса
 
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложении
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложенииRambler.iOS #7: Прием платежей по банковским картам в iOS приложении
Rambler.iOS #7: Прием платежей по банковским картам в iOS приложении
 
Rambler.iOS #7: Интернет-эквайринг 101
Rambler.iOS #7: Интернет-эквайринг 101Rambler.iOS #7: Интернет-эквайринг 101
Rambler.iOS #7: Интернет-эквайринг 101
 

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

  • 2. What makes a clean test? Three things. Readability, readability, and readability. Robert C. Martin, «Clean Code»
  • 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
  • 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
  • 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