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
280
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
320
OKR без хайпа
etolstoy
1
290
OKR: инструкция по применению
etolstoy
0
380
Avito Mobile: State of the Union
etolstoy
0
120
Developer Experience: The Art of Building Spaceships
etolstoy
1
600
Выступайте
etolstoy
0
140
Улучшая performance review
etolstoy
0
1.4k
May the Code Review be with you [English]
etolstoy
1
230
May the Code Review be with you [Russian]
etolstoy
1
190
Other Decks in Technology
See All in Technology
いま現場PMのあなたが、 経営と向き合うPMになるために 必要なこと、腹をくくること
hiro93n
9
7.9k
あなたの人生も変わるかも?AWS認定2つで始まったウソみたいな話
iwamot
3
870
20250116_自部署内でAmazon Nova体験会をやってみた話
riz3f7
1
110
[IBM TechXchange Dojo]Watson Discoveryとwatsonx.aiでRAGを実現!座学①
siyuanzh09
0
110
メンバーがオーナーシップを発揮しやすいチームづくり
ham0215
2
240
Cloudflareで実現する AIエージェント ワークフロー基盤
kmd09
0
290
VPC Block Public AccessとCloudFrontVPCオリジンによって何が変わるのか?
hatahata021
2
100
AWS Community Builderのススメ - みんなもCommunity Builderに応募しよう! -
smt7174
0
190
Copilotの力を実感!3ヶ月間の生成AI研修の試行錯誤&成功事例をご紹介。果たして得たものとは・・?
ktc_shiori
0
370
AWSサービスアップデート 2024/12 Part3
nrinetcom
PRO
0
150
自社 200 記事を元に整理した読みやすいテックブログを書くための Tips 集
masakihirose
2
350
今から、 今だからこそ始める Terraform で Azure 管理 / Managing Azure with Terraform: The Perfect Time to Start
nnstt1
0
250
Featured
See All Featured
Visualization
eitanlees
146
15k
What's in a price? How to price your products and services
michaelherold
244
12k
How to Ace a Technical Interview
jacobian
276
23k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
29
2.4k
Statistics for Hackers
jakevdp
797
220k
Rebuilding a faster, lazier Slack
samanthasiow
79
8.8k
Building a Modern Day E-commerce SEO Strategy
aleyda
38
7k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
365
25k
Making the Leap to Tech Lead
cromwellryan
133
9k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
28
2.2k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
7
570
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
26
1.9k
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