iOSでのテストによる開発

内容:







テストによる開発-それは何ですか?



テスト駆動開発とは、テストの作成を通じて開発を定義するソフトウェア開発手法です。 基本的に、次の3つの簡単な繰り返し手順を実行する必要があります。

-追加する必要がある新しい機能のテストを作成します。

-テストに合格するコードを記述します。

-新しいコードと古いコードをリファクタリングします。



マーティン・ファウラー










TDDの3つの法則



現在、多くの人々は、TDDが実動コードを作成する前に最初にテストを作成する必要があることを知っています。 しかし、このルールは氷山の一角にすぎません。 次の3つの法律を考慮することをお勧めします。

  1. 本番コードは、アイドルテストが行​​われる前に作成されません。
  2. エラーに十分な数の単体テストコードは書き込まれません。 そして、コンパイルではなく間違いです。
  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では、コードの機能を確認するテストです。







例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); }]; }
      
      





長所と短所



メリット






短所










テストによる開発は、シンプルな設計を促進し、自信を呼び起こします。

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



All Articles