Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Clean Unit Tests
Search
Egor Tolstoy
October 05, 2016
Technology
0
260
Clean Unit Tests
The story about why unit tests matter.
Egor Tolstoy
October 05, 2016
Tweet
Share
More Decks by Egor Tolstoy
See All by Egor Tolstoy
Как подсидеть тимлида
etolstoy
0
280
OKR без хайпа
etolstoy
1
260
OKR: инструкция по применению
etolstoy
0
330
Avito Mobile: State of the Union
etolstoy
0
97
Developer Experience: The Art of Building Spaceships
etolstoy
1
550
Выступайте
etolstoy
0
110
Улучшая performance review
etolstoy
0
1.3k
May the Code Review be with you [English]
etolstoy
1
220
May the Code Review be with you [Russian]
etolstoy
1
190
Other Decks in Technology
See All in Technology
データ分析を支える技術 生成AI再入門
ishikawa_satoru
0
380
Azure AI ことはじめ
tsubakimoto_s
0
130
スレットハンティングについて知っておきたいこと
hacket
0
130
セキュリティ研修 Day1【MIXI 24新卒技術研修】
mixi_engineers
PRO
0
160
DevIO2024_レガシー運用からの脱却 -クラウド活用の実践事例とベストプラクティス-
jun2882
0
210
Classmethod Odyssey 登壇資料
yamahiro
0
390
Amazon FSx for NetApp ONTAPのパフォーマンスチューニング要素をまとめてみた #cm_odyssey #devio2024
non97
0
220
AIエージェントを現場に導入する目線とは
masahiro_nishimi
1
1.5k
How to Think Like a Performance Engineer
csswizardry
4
590
サービスの持続的な成長と技術負債について
siva_official
PRO
10
4.4k
AIアシスタントの活用で品質の向上と開発ワークフローのスピードアップ
nagix
1
210
楽しくGoを学び合う、LayerXの勉強会文化 / LayerX's study culture of having fun and learning Go together
ar_tama
2
350
Featured
See All Featured
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
35
6.3k
How GitHub (no longer) Works
holman
305
140k
For a Future-Friendly Web
brad_frost
173
9.2k
Automating Front-end Workflow
addyosmani
1362
200k
What's new in Ruby 2.0
geeforr
338
31k
Building Flexible Design Systems
yeseniaperezcruz
323
37k
The Illustrated Children's Guide to Kubernetes
chrisshort
39
47k
Practical Orchestrator
shlominoach
185
10k
Being A Developer After 40
akosma
72
580k
Speed Design
sergeychernyshev
9
270
It's Worth the Effort
3n
181
27k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
24
1.8k
Transcript
Чистые 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