内容:
- テストによる開発-それは何ですか?
- TDDの3つの法則
- 応用例
- 長所と短所
- 文献と参考文献
テストによる開発-それは何ですか?
テスト駆動開発とは、テストの作成を通じて開発を定義するソフトウェア開発手法です。 基本的に、次の3つの簡単な繰り返し手順を実行する必要があります。
-追加する必要がある新しい機能のテストを作成します。
-テストに合格するコードを記述します。
-新しいコードと古いコードをリファクタリングします。
マーティン・ファウラー
![](https://habrastorage.org/files/ecd/be6/686/ecdbe66867f245008fb0db3d12d54222.png)
TDDの3つの法則
現在、多くの人々は、TDDが実動コードを作成する前に最初にテストを作成する必要があることを知っています。 しかし、このルールは氷山の一角にすぎません。 次の3つの法律を考慮することをお勧めします。
- 本番コードは、アイドルテストが行われる前に作成されません。
- エラーに十分な数の単体テストコードは書き込まれません。 そして、コンパイルではなく間違いです。
- 現在のアイドルテストに合格するのに十分な量のプロダクションコードは書き込まれません。
これらの3つの法律により、プログラマは約1分間のサイクルを維持します。 テストと本番コードは同時に作成され、テストは本番コードの数秒前になります。
この方法で作業する場合、1日に数十、毎月数百、毎年数千のテストを作成します。 この方法で作業する場合、テストは製品コードを完全にカバーします。
応用例
例1.モデルオブジェクトの並べ替え
モデルオブジェクト(イベント)を並べ替えるメソッドを記述する最も単純な例を考えてみましょう。 最初に、新しいメソッドのインターフェイスを記述します(入力としてオブジェクトの無秩序な配列を受け取り、それに応じてソートを返します)。
テストメソッドインターフェース:
- (NSArray *)sortEvents:(NSArray *)events
次に、テストを記述します。
与えられた表記法を使用して、テストを3つの論理ブロックに分割します。
- 環境の作成、テストの前提条件。
- テストメソッドを呼び出してアクションをコミットします。
- テストされた機能の機能を確認します。
テスト:
- (void)testSortEvents { // given id firstEvent = [self mockEventWithClass:kCinemaEventType name:@""]; id secondEvent = [self mockEventWithClass:kCinemaEventType name:@""]; id thirdEvent = [self mockEventWithClass:kPerfomanceEventType name:@""]; id fourthEvent = [self mockEventWithClass:kPerfomanceEventType name:@""]; NSArray *correctSortedArray = @[firstEvent, secondEvent, thirdEvent, fourthEvent]; NSArray *incorrectSortedArray = @[thirdEvent, secondEvent, fourthEvent, firstEvent]; // when NSArray *sortedWithTestingMethodArray = [self.service sortEvents:incorrectSortedArray]; // then XCTAssertEqualObjects(correctSortedArray, sortedWithTestingMethodArray, @" "); }
テストが成功しないことを確認した後にのみ、メソッドの実装を記述します。
メソッドの実装:
- (NSArray *)sortEvents:(NSArray *)events { // ( , , ) NSMutableArray *cinemaEvents = [[NSMutableArray alloc] init]; NSMutableArray *otherEvents = [[NSMutableArray alloc] init]; for (Event *event in events) { if ([event.type isEqualToString:kCinemaEventType]) { [cinemaEvents addObject:event]; } else { [otherEvents addObject:event]; } } NSComparisonResult (^sortBlock)(Event *firstEvent, Event *secondEvent) = ^NSComparisonResult(Event *firstEvent, Event *secondEvent) { return [firstEvent.type compare:secondEvent.type options:NSNumericSearch]; }; [cinemaEvents sortUsingComparator:sortBlock]; [otherEvents sortUsingComparator:sortBlock]; return [cinemaEvents arrayByAddingObjectsFromArray:otherEvents]; }
メソッドの実装を記述した後、新しいものを含むすべてのテストが正常に実行されたことを確認します。 必要に応じて、記述されたコードをリファクタリングします。 リファクタリングは緑の状態から緑にのみ実行できることを理解することが重要です。 すべてのテストが正常に完了したとき。
TDDの非常に重要な機能は、責任の逆転です。 ソフトウェア開発への古典的なアプローチでは、プログラマーは、コードを書いた後、個人的にテストし、デバッガーでコードを駆動し、すべてのifの動作をチェックします。 TDDでは、コードの機能を確認するテストです。
![](https://habrastorage.org/files/5a1/59a/a2a/5a159aa2acd549f0a8debf2cccbf720b.png)
例2.マッパー
2番目の例を考えてみましょう-モデルオブジェクトのサーバー出力マッパー。
マッパーは次の要件を満たしている必要があります。
1)オブジェクトをマップする(予期せず)
2)出力曲線の処理(Null、無効なデータ型、誤った出力構造)
3)データの一貫性を確保する(必須フィールドの検証)
テストされたメソッドのインターフェースを作成します、それは同期的です:
テストメソッドインターフェース:
- (NSArray *)mapEvents:(id)responseObject
次に、肯定的なシナリオをチェックする最初のテスト、つまり 適切に発行されたオブジェクトのマッピング。 すべてのテストを一度に作成するわけではなく、マッパーが満たす必要のあるすべての要件をカバーしようとはせず、最も単純な実装のための唯一のテストを作成します。
テスト:
- (void)testMapEventsResponseSuccessful { // given NSDictionary *responseObject = @{@"event" : @[@{@"type" : @1, @"name" : @"test", @"description" : @"test"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; [self checkObject:event forKeyExistence:@[@"type", @"name", @"description"]]; }
オブジェクトの必須フィールドの存在を確認するには、KVCヘルパーメソッドを使用します。
- (void)checkObject:(id)object forKeyExistence:(NSArray *)keys { for (NSString *key in keys) { if (![object valueForKey:key]) { XCTFail(@" - %@", key); } } }
次に、テストに適合するメソッドの最も簡単な実装を記述し、テストが成功することを確認します。 この場合、内部のマッパーはRESTKitのマッピングエンジンを使用しますが、メソッドをブラックボックスと見なし、その実装は将来変更される可能性があります。 ただし、ほとんどの場合の要件はそのままです。
メソッドの実装:
- (NSArray *)mapEvents:(id)responseObject { NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]}; RKMappingResult *mappingResult = [self mapResponseObject:responseObject withMappingsDictionary:mappingsDictionary]; if (mappingResult) { return [mappingResult array]; } return @[]; }
さて、最も単純な実装を作成した後、マッパーに別のテストを追加します。これは、マッパーが出力の一部のフィールドの必須性を考慮することを確認します。この場合、テストにないのはidフィールドです。 テストのタスクは、クラッシュがないことを確認することであり、マッピング後にメソッドは空の配列を返します。
テスト:
- (void)testMapEventsResponseWithMissingMandatoryFields { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"name" : @"test"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then XCTAssertFalse([events count]); }
次に、メソッドを拡張し、必須フィールドのチェックを追加します。
メソッドの実装:
- (NSArray *)mapEvents:(id)responseObject { NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]}; RKMappingResult *mappingResult = [self mapResponseObject:responseObject withMappingsDictionary:mappingsDictionary]; if (mappingResult) { // NSArray *events = [self checkMandatoryFields:[mappingResult array]]; return events; } return @[]; }
出力にNullがある場合、初期要件に基づいて、マッパーは正しく動作するはずです。これをチェックするテストを作成します。
- (void)testMapEventsResponseWithNulls { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @1, @"name" : [NSNull null], @"description" : [NSNull null]}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; XCTAssertNotNil(event.type, @""); XCTAssertNil(event.name, @""); }
テストを作成して実行すると、すぐに正常に実行されることがわかります。 はい、これも起こります。既存のコードはすでに新しい要件を満たしている可能性があります。 この状況では、もちろん、このテストを削除しません。 マッパーの実装が変更される可能性がある将来の彼の仕事は、彼の仕事の正確性を検証することです。 次に、数字ではなく文字列の間違ったデータ型のテストを追加します。 RestKitのマッピングエンジンもこれを処理できるため、テストは再び緑色に変わります。
- (void)testMapEventsResponseWithWrongType { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @"123"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; XCTAssertTrue([event.type isEqual:@123]); }
例3.イベント受信サービス
モデルオブジェクト(イベント)を取得するサービスを検討すると、プロトコルには次のメソッドが含まれます。
@interface EventService : NSObject - (instancetype)initWithClient:(id<Client>)client mapper:(id<Mapper>)mapper; - (NSArray *)obtainEventsForType:(EventType)eventType; - (void)updateEventsForType:(EventType)eventType success:(SuccessBlock)success failure:(ErrorBlock)failure; @end
このサービスはCoreDataで動作するため、テストのためにメモリ内のCoreDataスタックを初期化する必要があります。 MagicDataRecordライブラリを使用してCoreDataを操作します。ティアダウンで[MagicalRecord cleanUp]を呼び出すことを忘れないでください。 ネットワーククライアントとマッパーがサービスに挿入されるため、mokiを初期化子に転送します。
@interface EventServiceTests : XCTestCase @property (nonatomic, strong) EventService *eventService; @property (nonatomic, strong) id<Client> clientMock; @property (nonatomic, strong) id<Mapper> mapperMock; @end @implementation EventServiceTests - (void)setUp { [super setUp]; [MagicalRecord setDefaultModelFromClass:[self class]]; [MagicalRecord setupCoreDataStackWithInMemoryStore]; self.clientMock = OCMProtocolMock(@protocol(Client)); self.mapperMock = OCMProtocolMock(@protocol(Mapper)); self.eventService = [[EventService alloc] initWithClient:self.clientMock mapper:self.mapperMock]; } - (void)tearDown { self.eventService = nil; self.clientMock = nil; self.mapperMock = nil; [MagicalRecord cleanUp]; [super tearDown]; }
取得メソッドのテストを追加します-データベースからオブジェクトを同期的に受信します。
- (void)testObtainEventsForType { // given Event *event = [Event MR_createEntity]; event.eventType = EventTypeCinema; // when NSArray *events = [self.eventService obtainEventsForType:EventTypeCinema]; // then XCTAssertEqualObjects(event, [events firstObject]); }
getメソッドとサービス初期化子の実装:
- (instancetype)initWithClient:(id<Client>)client mapper:(id<Mapper>)mapper { self = [super init]; if (self) { _client = client; _mapper = mapper; } return self; } - (NSArray *)obtainEventsForType:(EventType)eventType { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"eventType = %d", eventType]; NSArray *events = [Event MR_findAllWithPredicate:predicate]; return events; }
更新メソッドのテストを追加します。クライアントを使用してサーバーにイベントを要求し、次にモデルにマップピットを要求して、データベースに保存します。 クライアントを停止して、常に成功ブロックを返すようにします。 サービスがマッパーに連絡して成功ブロックを呼び出すことを確認します。
- (void)testUpdateEventsForTypeSuccessful { // given XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"]; OCMStub([self.clientMock requestEventsForType:EventTypeCinema success:OCMOCK_ANY failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) { SuccessBlock block; [invocation getArgument:&block atIndex:3]; block(); }); // when [self.eventService updateEventsForType:EventTypeCinema success:^{ [expectation fulfill]; } failure:^(NSError *error) { }]; // then [self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil]; OCMVerify([self.mapperMock mapEvents:OCMOCK_ANY forType:EventTypeCinema mappingContext:OCMOCK_ANY]); }
updateメソッドのエラースクリプトのテストを記述し、エラーを返すようにクライアントを安定させます。
- (void)testUpdateEventsForTypeFailure { // given XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"]; NSError *clientError = [NSError errorWithDomain:@"" code:1 userInfo:nil]; OCMStub([self.clientMock requestEventsForType:EventTypeCinema success:OCMOCK_ANY failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) { ErrorBlock block; [invocation getArgument:&block atIndex:4]; block(error); }); // when [self.eventService updateEventsForType:EventTypeCinema success:^{ } failure:^(NSError *error) { XCTAssertEqual(clientError, error) [expectation fulfill]; }]; // then [self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil]; }
更新メソッドの実装:
- (void)updateEventsForType:(EventType)eventType success:(SuccessBlock)success failure:(ErrorBlock)failure { @weakify(self); [self.client requestEventsForType:eventType success:^(id responseObject) { @strongify(self); NSManagedObjectContext *context = [NSManagedObjectContext MR_rootSavingContext]; NSArray *events = [self.mapper mapEvents:responseObject forType:eventType mappingContext:context]; [context MR_saveToPersistentStoreAndWait]; success(); } failure:^(NSError *error) { failure(error); }]; }
長所と短所
メリット
- 実装前にインターフェイスを熟考する
主な利点の1つであるTDDを使用すると、プログラマーは最初に新しい機能の動作を詳細に検討し、それを作成する必要があります。
- 少ないデバッグ
テストでのコードカバレッジの割合が高いため、デバッガーで長時間を費やすことなく、エラーをより迅速に見つけることができます。
- 変化への自信
多くの人が1か所でコードを変更した状況に直面したと思いますが、何かがまったく異なる予期しない場所で壊れました。 もちろん、これはコード設計の質が低いことを理解することが重要です。 テストでのカバレッジは、既存のコードを変更またはリファクタリングするときに自信を与えます。なぜなら、緊急事態が発生すると、テストがすぐにそれをキャッチするからです。
- ミスが少ない
これは、TDDのその他の利点を組み合わせることで実現されます。主に、事前に実装を検討し、テストでカバーします。
- テスト文書
テストは、既存のコードベースであるプロジェクトドキュメントの一部です。 多くの場合、新しい開発者がプロジェクトに参加すると、特定の例を使用してコードを処理するのが簡単になり、テストはコードを使用する最も簡単な例です。 その結果、テストでコードを完全に網羅することで、本質的に広範囲にわたるコードの実用的なドキュメントが得られます。
- モジュール性
コード実行のすべてのブランチ、すべてのifを完全にテストする必要があるため、複数のスレッドで一度に10の処理を行う全能メソッドをテストすることはできないため、モジュール方式で記述する必要があります。 私たちは、各クラス、各メソッドが作業全体の小さな部分を確実に実行するよう努めています。 (単一責任の原則に従う)。
- 完全なテスト
多くの人がテストを書いたと思いますが、誰もがTDDで書いたわけではなく、違いは実際に目立っています。 実装後にテストを作成する場合、メソッドを実行するためのすべてのスクリプトを考慮に入れることはできず、最終的に欠陥のあるテストを取得することはできません。 テストが機能するためにメソッドが機能すると信じていますが、実際には、すべてのifがメソッドに含まれていないため、TDDではテストなしではコードに表示できないため、これは不可能です。
短所
- 使用の難しさ(データセキュリティ、UI、データベース)
もちろん、多くの場合、テストが困難または不可能であることがわかります。 一部のモジュールがどのように機能するかをすぐに想像するのが難しいことがあります。
- もっと時間
TDDでの書き込みを開始すると、開発時間が最大で数倍に増加する場合があります。 しかし、文字通り数ヶ月でそれなしで書くことは難しくなります。 将来的には、テストの記述とTDDの使用の経験により、開発時間が短縮されます。 これは、長命のプロジェクトでは特に遠くで感じられます。
- 信頼性の誤り、テストのエラー
初期要件を誤解した場合に、状況が発生する可能性があります。 その結果、コード、テスト、および理解においてエラーが発生します。 そして最も重要なことは、テストが緑色で点灯しているため、すべてが機能していることを確認しています。 これはかなり危険な状況であり、一時的に大きな損失につながる可能性があります。
- テストサポート
コードがテストで完全にカバーされると、コードベースはほぼ2倍になります。 また、このコードはすべてサポート、リファクタリング、文書化する必要があることを理解することが重要です。
![](https://habrastorage.org/files/82b/e19/eed/82be19eed59543119f1b6c1b13dfe929.png)
テストによる開発は、シンプルな設計を促進し、自信を呼び起こします。
TDDはシンプルなデザインを奨励し、自信を呼び起こします。
ケント・ベック
文献と参考文献
ケントベック-テスト駆動開発:例
Robert C. Martin-クリーンコード
www.objc.io/issue-15
en.wikipedia.org/wiki/Test-driven_development
qualitycoding.org/objective-c-tdd
agiledata.org/essays/tdd.html#WhatIsTDD
www.basilv.com/psd/blog/2009/test-driven-development-benefits-limitations-and-techniques
martinfowler.com/articles/is-tdd-dead
habrahabr.ru/post/206828
habrahabr.ru/post/216923
iosunittesting.com