iOSはテスト駆動の方法でリストモジュールを開発しようと試みた【三】

13808 ワード

この文章は引き続き第【二】の文章が完成していない部分を開発し、データソースエージェントクラスに表ビューにデータと応答エージェントを提供させることを実現し、前の文章では「(3)表ビューの行数、行高、Cellがそのデータソースエージェントクラスによって提供されたデータと一致していることを確認する」ことをテスト検証することについて述べた.具体的にはどうすればいいですか?
答えは意外なことに、それは「できない」か「そうしないで」です.
ユニットテストは多くのテスト技術ツールの1つにすぎず、独自の限界があり、UIに関するテストには適していないことが明らかになった.例えば、ここ(3)でテストするものである.理由:1,UIが多変し,テスト用例が不安定になるためである.2,UIの実装方式は多様で,必ずしもコードに反映されないものもあり,例えばxib方式で実装する場合,コードを測定しても何の役にも立たない.3、コストが高くて、ユニットテストでUIを測定するには、間接的に測定するために様々な曲がりを迂回しなければならないに違いない.それは、viewを画面に表示させ、測定されたサブviewのframeがテスト例を実行するときに価値があることを保証するのは容易ではない.そのため、ユニットテストでUIを測定する必要はありません.以上の3つの原因に加えて、現在のこの表ビューのテストに対して、もう1つの原因があります.それは、システムのフレームワークを測定するクラスをユニットでテストしないことです.ここで私たちのテーブルビューはシステムフレームワークのクラスで、デフォルトでは常に正常です.データソースから提供されたデータを取得した後、データの要求に従って正しい行数、行の高さ、正常なCellを表示できるかどうかをテストしないでください.これらの論理は表ビュークラス自体の論理であるため、明らかにこの部分の仕事はアップルが私たちのためにしてくれたので、私たちはもうそれらをテストしないでください.
では、表の周りに何かテストできるものはありますか?あります.そして、必ずテストされる部分です.それは、テーブルビューが正しいデータソースとエージェントクラスを手に入れることを保証することです.私たちは第【2】の文章でユニットテストの例を使って、表のビューが私たちが提供したデータソースとエージェントクラスを手に入れることを保証しましたが、このデータソースとエージェントクラスは「正しい」かどうか、私たちはまだテストしていないので、この文章では、このデータソースとエージェントクラスの内部論理が正しいことをテストします.これをやり終えて、私たちは(3)のテストを完成しました.我々は(3)のタスクを「テーブルビューのデータソースとエージェントクラスがテーブルに正しい行数、正しいCellsを提供したことを確認し、テーブルのCellクリックイベントに正しい応答をした」に変更した.
まず、データソース自体はデータを生成せず、そのデータも他の場所から取得されるので、デジタルソースの正確性とは、テーブルに提供されるデータが他の場所から取得されたデータと一致しているかどうかです.MyTableViewDataSourceTestsにテスト例を追加します.
【tc 3.1,tc 3.2,tc 3.3,テストデータソースがテーブルに提供するデータ個数は、他の場所から取得したデータ個数と同じである】
/**
 tc 3.1
 */
- (void)test_ProvideZeroRowCountWithNilOrEmptyDataArray{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    dataSource.theDataArray = nil;
    NSInteger rowCount = [dataSource tableView:tableView numberOfRowsInSection:0];
    XCTAssertEqual(rowCount, 0);
    dataSource.theDataArray = @[];
    rowCount = [dataSource tableView:tableView numberOfRowsInSection:0];
    XCTAssertEqual(rowCount, 0);
}

/**
 tc 3.2
 */
- (void)test_ProvideOneRowCountWithDataArrayContainsOneElement{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    dataSource.theDataArray = @[@"first row"];
    NSInteger rowCount = [dataSource tableView:tableView numberOfRowsInSection:0];
    XCTAssertEqual(rowCount, 1);
}

/**
 tc 3.3
 */
- (void)test_ProvideTwoRowCountWithDataArrayContainsTwoElement{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    dataSource.theDataArray = @[@"first row",@"second row"];
    NSInteger rowCount = [dataSource tableView:tableView numberOfRowsInSection:0];
    XCTAssertEqual(rowCount, 2);
}

これはRedフェーズなので、例によっては失敗し、コンパイルすることもできません.現在のデータ・ソース・エージェント・クラスにはtheDataArrayという属性がありません.コードを補完し、RedフェーズをGreenフェーズに進める.

#import 

@interface MyTableViewDataSource : NSObject

@property (nonatomic, strong) NSArray *theDataArray;

@end

//.m 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return [self.theDataArray count];
}

このように、上記の3つのテスト例はすべて合格し、他の前の例を失敗させなかったので、ここで(3)の最初のテスト、データソースの個数の正確性テストを完了しました.
次に、2番目のテストを行い、データソースをテストしてテーブルに正しいCellsを提供します.正しいセルとは?それはIndexPath対応のCellが示すデータがtheDataArray内の対応する要素のデータと一致しているということです.表cellを描く方法はcellにMyModelの属性を持たせ、cellに異なるmodelを割り当てることで異なる内容を描くことですが、modelはデータソースクラスが外部から取得したtheDataArray辞書の配列に基づいて順番にカプセル化され、各modelはそれぞれ対応する順序のcellに割り当てられます.cellはタイトル、タイプのピクチャを表示し、テスト時に、ピクチャが一致しているかどうかを検証してピクチャアドレスが等しいかどうかを検証する.このdemoのため、ピクチャの取得は固定アドレスで取得するように設計した.ピクチャurlはインタフェースから取得するのではなく、インタフェースから取得したtypeに基づいて、MyModel内部で対応するpicUrlに出力すると判断したので、cellのピクチャがデータソースに対応するピクチャであるかどうかを比較する.そのtypeとデータソースのtypeが一致しているかどうかを比較するだけでよいが、第【一】の文章では、MyModelがtypeに基づいて対応する画像のurlを取得できることをテストした.一方、models間ではtitle、picUrl、typeにかかわらずこれらの属性値は同じであるため、modelの一意性を判別するためにid属性を導入する必要があり、idはmodelの一意性のタグである.MyModelクラスにsomeId、titleの2つのプロパティを追加します.
#import 

typedef NS_ENUM(NSUInteger, ModelType){
    ModelTypeA = 0,
    ModelTypeB,
    ModelTypeC
};

@interface MyModel : NSObject

@property (nonatomic, copy) NSString *someId;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, assign) ModelType type;
@property (nonatomic, copy) NSString *picUrl;

@end

【tc 3.4、テストデータソースは表にMyCellタイプのcellオブジェクトを返す】
//MyTableViewDataSourceTests.m

/**
 tc 3.4
 */
- (void)test_ProvideMyCellInstance{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    XCTAssertTrue([cell isKindOfClass:[MyCell class]]);
}

新しいMyCellクラス:
#import 
#import "MyModel.h"

@interface MyCell : UITableViewCell

@property (nonatomic, strong) MyModel *model;

@end

データソースクラスの提供cellメソッドの実装を変更し、テストを通過させます.
// MyTableViewDataSource.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    return [[MyCell alloc] init];
}

【tc 3.5、最初のcellをテストして得たデータが最初のデータです】【tc 3.6、2番目のcellをテストして得たデータが2番目のデータです】
//  MyTableViewDataSourceTests

/**
 tc 3.5
 */
- (void)test_FirstCellHasFirstDataModel{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    dataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"}];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    MyCell *cell = (MyCell *)[dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    XCTAssertTrue([cell.model.someId isEqualToString:@"0001"]);
    XCTAssertTrue(cell.model.type == ModelTypeA);
    XCTAssertTrue([cell.model.picUrl isEqualToString:@"AUrl"]);
    XCTAssertTrue([cell.model.title isEqualToString:@"Type A Title"]);
}

/**
 tc 3.6
 */
- (void)test_SecondCellHasSecondDataModel{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    dataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:0];
    MyCell *cell = (MyCell *)[dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    XCTAssertTrue([cell.model.someId isEqualToString:@"0002"]);
    XCTAssertTrue(cell.model.type == ModelTypeB);
    XCTAssertTrue([cell.model.picUrl isEqualToString:@"BUrl"]);
    XCTAssertTrue([cell.model.title isEqualToString:@"Type B Title"]);
}


データソースクラスのcellを提供する方法の実装を変更し、上記の2つのテストを通過させます.
//  MyTableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    MyModel *model = [[MyModel alloc] init];
    model.someId = self.theDataArray[indexPath.row][@"someId"];
    model.type = [self.theDataArray[indexPath.row][@"type"] integerValue];
    model.title = self.theDataArray[indexPath.row][@"title"];
    MyCell *cell = [[MyCell alloc] init];
    cell.model = model;
    return cell;
}

ここで、(3)の2番目のテストポイント「テストデータソースはテーブルに正しいCellsを提供した」が完了し、各行のcellが対応するデータソースを使用することを保証しました.しかし、これでは十分ではありません.cellの提供方法(-(UItableView)tableView:(UItableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath)の現在の実装では、Cellの再利用メカニズムは使用されていません.これはテーブルビューの使用にとって重要なので、この点を上書きするためにテスト例を多く書きます.MyCellのreuseIdentifierをMyCellのクラスメソッドで取得できる文字列に設計し、MyCellの場所さえ使えば同じreuseIdentifierを取得することができます.MyCellクラスがテーブルオブジェクトの再利用可能Cellとして登録されている場合、テーブルオブジェクトはキューからcellを取得する方法[-(nullable__kindofUItableView Cell*)d e q u e R u s a bleCellWithIdentifier:(NSString*)identifier]で再利用可能オブジェクトを取得でき、再利用可能cellがない場合、自動的に新しいcellが作成されます.逆に登録していない場合は、このメソッドを呼び出すとnilが返されます.データソースのcell提供メソッドに再利用メカニズムを使用すると、テーブルオブジェクトがMyCellに登録されている場合、このメソッドを呼び出すとMyCellオブジェクトが取得されます.表オブジェクトがMyCellに登録されていない場合、このメソッドを呼び出すとnilが返されます.この考え方を用いて,データソースがCell再利用機構を使用しているかどうかについてのテストケースを書いた.
Redプロセスを実行します.失敗したテスト例を書きます.【tc 3.7】現在の実装では通過できるが、【tc 3.8】は失敗する.【tc 3.7、再利用セルを登録してcellオブジェクトを入手可能】【tc 3.8、再利用セルを登録していないとcellオブジェクトを入手できない】
//  MyTableViewDataSourceTests.m

/**
 tc 3.7
 */
- (void)test_ProvideCellIfTableViewRegistedReusableCell{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    [tableView registerClass:[MyCell class] forCellReuseIdentifier:[MyCell reuseIdentifier]];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    XCTAssertNotNil(cell);
    XCTAssertTrue([cell isKindOfClass:[MyCell class]]);
}

/**
 tc 3.8
 */
- (void)test_DoNotProvideCellIfTableViewDoNotRegistedReusableCell{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    XCTAssertNil(cell);
}

Greenプロセスを実行し、テストを通過させます.MyCellの再利用識別子の取得方法を追加します.
//  MyCell.h
+ (NSString *)reuseIdentifier;

//  MyCell.m
+ (NSString *)reuseIdentifier{
    return @"MyCell";
}

データソースクラスのcell提供方法の実装を変更します.
//  MyTableViewDataSource.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    MyModel *model = [[MyModel alloc] init];
    model.someId = self.theDataArray[indexPath.row][@"someId"];
    model.type = [self.theDataArray[indexPath.row][@"type"] integerValue];
    model.title = self.theDataArray[indexPath.row][@"title"];
    MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:[MyCell reuseIdentifier]];
    cell.model = model;
    return cell;
}

MyTableViewDataSourceTests全体を再実行します.mファイルのテスト例では、【tc 3.7,tc 3.8】に合格したが、前の【tc 3.4,tc 3.5,tc 3.6】はまた失敗した.これは当然予想外であり、結局、それらが使用している表オブジェクトはCellを登録して再利用していないので、cell対象を取得できない.しかし、今回のグリーンプロセスは完成した.次に、失敗した使用例を修復し、setUpメソッドで実行できる冗長コードが多く含まれているため、この時点でテストコードを再構築できます.そのため、Refactorプロセスを実行します.テストファイルMyTableViewDataSourceTestsのコードを修正し、「tc 3.8」を除いて他の使用例の共通部分コードをsetUpメソッドに抽出して実行します.
@interface MyTableViewDataSourceTests : XCTestCase

@property (nonatomic, strong) MyTableViewDataSource *dataSource;
@property (nonatomic, strong) UITableView *theTableView;

@end

@implementation MyTableViewDataSourceTests

- (void)setUp {
    [super setUp];
    self.dataSource = [[MyTableViewDataSource alloc] init];
    self.theTableView = [[UITableView alloc] init];
    [self.theTableView registerClass:[MyCell class] forCellReuseIdentifier:[MyCell reuseIdentifier]];
}

- (void)tearDown {
    self.dataSource = nil;
    self.theTableView = nil;
    [super tearDown];
}

すべてのテストケースを再実行し、合格しました.今回のRefactorプロセスが成功したことを証明します.実装後の方法を変更することによって(-(UItableView*)tableView:(UItableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath)表オブジェクトに再利用可能なCellクラスの登録を強制するが、登録を忘れたらどうなるのだろうか.実際に実行するとこの方法は崩壊します.したがって,メソッドに断言を設定し,データソースオブジェクトの使用テーブル登録再利用可能Cellを強制する.Cellが登録されていない場合、この方法を実行すると異常が発生することをテスト例によって保証します.
//  MyTableViewDataSourceTests

/**
 tc 3.9
 */
- (void)test_AssertFailureIfTableViewDidNotRegistReuseCell{
    UITableView *tableView = [[UITableView alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    XCTAssertThrows([dataSource tableView:tableView cellForRowAtIndexPath:indexPath]);
}

データ・ソースのcell提供メソッド・コードを変更して、この例を通過させます.
\\  MyTableViewDataSource.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    MyModel *model = [[MyModel alloc] init];
    model.someId = self.theDataArray[indexPath.row][@"someId"];
    model.type = [self.theDataArray[indexPath.row][@"type"] integerValue];
    model.title = self.theDataArray[indexPath.row][@"title"];
    MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:[MyCell reuseIdentifier]];
    NSAssert(cell, @" MyCell ");
    cell.model = model;
    return cell;
}

すべてのテストを再実行したところ、「tc 3.8」が失敗したことがわかりました.コードUITableViewCell *cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];を実行すると異常がトリガーされるためです.このテストケースは,本来,表オブジェクトがCellの再利用を登録していない場合,データソースのcell提供方法が失敗することをテストするために用いられていたが,現在では[tc 3.9]があれば,このテストケースを削除することができる.
【tc 3.1】~【tc 3.9】を経て、(3)の「テストデータソースはテーブルに正しい行数を提供した」と「テストデータソースはテーブルに正しいCellsを提供した」の2つの面でどのように重点部分に対してユニットテストを行うことができるかを実証した.実際の製品のテスト要件に達するまで.しかし、demoだけでも、1、ユニットテストは論理のみを測定し、UIを測定し、表示層に関連する論理を測定する場合、データソースクラスが表示層のクラスに正しいデータを提供したかどうかに重点を置く経験が少なくありません.2、テストドライバのやり方は私达の制品コード、机能をテスト用例の増加に従って次第に强化されて、私达は制品の実现コードが最初は简単なテスト用例を通すことができるため、あまりにも粗末で(甚だしきに至っては役に立たない)を実现して、后続のテスト用例のカバーがますます深くなってますます広くなる时、相応の制品コードも绝えず改善するためです.例えば、私たちのテーブルデータソースクラスのcell提供方法は、「tc 3.4」から「tc 3.9」まで、私たちのニーズに合致するまで改善されています.
続きます...demo: https://github.com/zard0/TDDListModuleDemo.git