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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Egor Tolstoy
October 05, 2016
Technology
350
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Clean Unit Tests
The story about why unit tests matter.
Egor Tolstoy
October 05, 2016
More Decks by Egor Tolstoy
See All by Egor Tolstoy
Как подсидеть тимлида
etolstoy
0
450
OKR без хайпа
etolstoy
1
380
OKR: инструкция по применению
etolstoy
0
660
Avito Mobile: State of the Union
etolstoy
0
190
Developer Experience: The Art of Building Spaceships
etolstoy
1
710
Выступайте
etolstoy
0
230
Улучшая performance review
etolstoy
0
1.6k
May the Code Review be with you [English]
etolstoy
1
290
May the Code Review be with you [Russian]
etolstoy
1
250
Other Decks in Technology
See All in Technology
事業価値と Engineering 2026年度版
recruitengineers
PRO
50
24k
Service Connect 上のサービスに ECS Service の外側から到達できなかった話
ota1022
1
260
Bits Agent Builder の⼊⾨と活⽤事例
nulabinc
PRO
0
170
社内の7割が使うデータ基盤を、 データチーム2人で回すためにやったこと
koh_yoshi
4
1.4k
会社紹介資料 / Sansan Company Profile
sansan33
PRO
24
430k
QAエンジニア起点で進める、SmartHRにおける信頼性向上について
kaomi_wombat
1
150
Software Supply Chain Attackからクラウド環境を守るためにできること
lhazy
2
270
生成 AI の基礎 〜 サンプル実装で学ぶ基本原理
enakai00
7
4.4k
システム思考で問題に対処する
yussak
0
310
2026-08-05 IBM Z開発最前線!IBM Bob Premium Package for Zって何がすごいの?
yutanonaka
0
140
Master Dataグループ紹介資料
sansan33
PRO
1
4.8k
AIペネトレーションテスト・ セキュリティ検証「AgenticSec」紹介資料
laysakura
2
9.1k
Featured
See All Featured
Understanding Cognitive Biases in Performance Measurement
bluesmoon
32
3k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
Speed Design
sergeychernyshev
33
2k
From π to Pie charts
rasagy
0
290
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.2k
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
230
What does AI have to do with Human Rights?
axbom
PRO
1
2.3k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.4k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.4k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
790
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
610
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