以前の記事では、単体テストについて検討しましたが、今回は統合テストについて説明します。
サンプルが大きくなりすぎず、素材も含まれているため、
RSSリーダーのサンプル部分を作成することにしました。
サーバーからの偽の応答は、作業オプションを確認するために考慮されます。
CoreDataを使用したテストが検討されます。
理論のいくつかの言葉:
単体テスト-システム内の1つの要素の動作を分離して確認します。
統合テスト-システムの一部の動作を一緒にチェックします。
XCTに慣れていない場合は、
ここで書きました。
インタラクションの基本ロジックが結論付けられる
SOA(サービス指向アーキテクチャ)を使用し
ます 。 実際、サービスはテストの主要な目標です。
また、main.mに変更が加えられ、メインターゲットの動作に関係なくテストが実行されます。
int main(int argc, char * argv[]) { @autoreleasepool { Class appDelegateClass = (NSClassFromString(@"XCTestCase") ? [RSTestingAppDelegate class] : [RSAppDelegate class]); return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass)); } }
RSTestCase基本クラスは、非同期コードをテストするための便利な方法など、テスト用に作成されました。
どうやって? typedef void (^RSTestCaseAsync)(XCTestExpectation *expectation); ... ... ... - (void)asyncTest:(RSTestCaseAsync)async { [self asyncTest:async timeout:5.0]; } - (void)asyncTest:(RSTestCaseAsync)async timeout:(NSTimeInterval)timeout { XCTestExpectation *expectation = [self expectationWithDescription:@"block not call"]; XCTAssertNotNil(async, @"don't send async block!"); async(expectation); [self waitForExpectationsWithTimeout:timeout handler:nil]; }
setUpメソッドの未処理の例外このクラスの主なポイントは、setUpメソッド内のフォールに関する情報を提供することです。 結局のところ、このメソッドはテストを実行する前に呼び出されます。ここでドロップすると、後続のテストが失敗します。 ただし、メソッド自体はテストではないため、メソッドをドロップしてもエラーはテストテーブルに書き込まれません。 したがって、このクラスにはtestInitAfterSetUpテストがあります。 このテストは成功し、setUpメソッドが成功すると、各子クラスによって(ランダムな順序で)呼び出されます。 このテストに失敗すると、setUpメソッド内でクラッシュが発生します。
統合テストはITグループに保存し、クラスはITの最後に保存します。
ユニットグループにユニットテストを保存し、テストが終了したクラスを保存します。
練習に取り掛かりましょう
CocoaPods依存関係
マネージャーから始めましょう
ポッドファイルプラットフォーム:ios、「8.0」
use_frameworks!
ポッド「AFNetworking」、「〜> 2.5.4」
ポッド「XMLDictionary」、「〜> 1.4」
ポッド「ReactiveCocoa」、「〜> 2.5」
ポッド「BlocksKit」、「〜> 2.2.5」
ポッド「MagicalRecord」、「〜> 2.3.0」
ポッド「MWFeedParser / NSDate + InternetDateTime」
ターゲット 'RSReaderTests' do
ポッド「OHHTTPStubs」、「〜> 4.6.0」
ポッド「OCMock」、「〜> 3.2」
終わり
ニュースフィードサービスをテストするために、RSFeedServiceIT.mファイルとRSFeedServiceITクラスを作成しましょう。
RSFeedServiceIT.m #import "RSTestCase.h" @interface RSFeedServiceIT : RSTestCaseIT @end @implementation RSFeedServiceIT - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } @end
次の場合に興味があります
1)RSSを取得
2)接続エラー
3)サーバーが見つかりません
そして、その3つの統合テスト。
そして、あなたがあなた自身のサーバーを持っていて、すべてのリクエストがそれに行くなら?サーバー用に作成している場合は、サーバーからRSSを受信するためのテストを1つ作成し、テストリストから除外できます(ただし、手動で実行します。サーバーまたは次のサーバーにすべて適合したら問題ありませんか?)。 これを行うには、テストリストで目的のテストを見つけてオフにします。
テストクラスでは、2つのフィールドが必要です。 テスト済みのサービスとURL。 各テストの前にこれを尋ねます。
RSFeedServiceIT.m @interface RSFeedServiceIT : RSTestCaseIT @property (strong, nonatomic) RSFeedService *service; @property (strong, nonatomic) NSString *url; @end ... ... ... - (void)setUp { [super setUp];
テスト1:RSSを取得
OHHTTPStubs-リクエストへの応答を偽造できます。 rss_news.xmlファイルからデータを返す必要がある要求の場合、Content-Typeはapplication / xmlであり、応答コードは200(OK)であると言います。
テストで回答を受け取ると、データが到着し、サービスが応答を正常に処理し、20のニュースを発行したことを確認します。
失敗ブロックを呼び出すと、テストエラーが発生するはずです。
testFeedFromURL #pragma mark test - (void)testFeedFromURL { [self stubXmlFeed]; [self asyncTest:^(XCTestExpectation *expectation) { @weakify(self); [self.service feedFromStringUrl:self.url success:^(NSArray *itemNews) { @strongify(self); [expectation fulfill]; XCTAssertNotNil(itemNews); XCTAssertEqual([itemNews count], 20); } failure:^(NSError *error) { @strongify(self); [expectation fulfill]; XCTFail(@"%@", error); }]; }]; } - (void)stubXmlFeed { [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return YES; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { NSString *xmlFeed = OHPathForFile(@"rss_news.xml", [self class]); NSDictionary *headers = @{ @"Content-Type" : @"application/xml" }; return [OHHTTPStubsResponse responseWithFileAtPath:xmlFeed statusCode:200 headers:headers]; }]; }
RSTestCaseIT親クラス(RSTestCaseの子孫)にメソッドを追加して、各テストの後にスタブを要求にリセットします。
- (void)tearDown { [OHHTTPStubs removeAllStubs];
また、RSTestCaseITにメソッドを追加して、ネットワーク要求でエラーを生成します。
stubHttpErrorDomain:コード:userInfo - (void)stubHttpErrorDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)userInfo { [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return YES; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { NSError *error = [NSError errorWithDomain:domain code:code userInfo:userInfo]; return [OHHTTPStubsResponse responseWithError:error]; }]; }
Test2:接続エラー
サービスは失敗ブロックを呼び出し、NSURLErrorNotConnectedToInternetコードとNSURLErrorDomainドメインでエラーを渡す必要があります。 成功ブロックを呼び出すと、テストエラーが発生します。
testFeedFromURLErrorInternet #pragma mark test - (void)testFeedFromURLErrorInternet { [self stubHttpErrorDomain:NSURLErrorDomain code:NSURLErrorNotConnectedToInternet userInfo:nil]; [self asyncTest:^(XCTestExpectation *expectation) { @weakify(self); [self.service feedFromStringUrl:self.url success:^(NSArray *itemNews) { @strongify(self); [expectation fulfill]; XCTFail(@"this is error"); } failure:^(NSError *error) { @strongify(self); [expectation fulfill]; XCTAssertEqualObjects([error domain], NSURLErrorDomain); XCTAssertEqual([error code], NSURLErrorNotConnectedToInternet); }]; }]; }
Test3:サーバーが見つかりません
testFeedFromURLErrorServerNotFound #pragma mark test - (void)testFeedFromURLErrorServerNotFound { [self stubHttpErrorDomain:NSURLErrorDomain code:NSURLErrorCannotFindHost userInfo:nil]; [self asyncTest:^(XCTestExpectation *expectation) { @weakify(self); [self.service feedFromStringUrl:self.url success:^(NSArray *itemNews) { @strongify(self); [expectation fulfill]; XCTFail(@"this is error"); } failure:^(NSError *error) { @strongify(self); [expectation fulfill]; XCTAssertEqualObjects([error domain], NSURLErrorDomain); XCTAssertEqual([error code], NSURLErrorCannotFindHost); }]; }]; }
ご覧のとおり、メソッドの呼び出し時にulrが渡されない場合、またはブロックが渡されない場合、システム要件の一部に見えるケースはここでは考慮されません。
そして今、小さなコード。 つまり、コードが単純化されます-コードを膨張させないために、専用のトランスポート層がありません。
RSFeedService #import <Foundation/Foundation.h> @interface RSFeedService : NSObject + (instancetype)sharedInstance; - (void)feedFromStringUrl:(NSString *)url success:(RSItemsBlock)success failure:(RSErrorBlock)failure; @end
#import "RSFeedService.h" #import "RSFeedParser.h" @interface RSFeedService () @property (strong, nonatomic) RSFeedParser *parser; @property (strong, nonatomic) AFHTTPRequestOperationManager *transportLayer; @end @implementation RSFeedService + (instancetype)sharedInstance { static dispatch_once_t onceToken; static RSFeedService *instance; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; instance.parser = [RSFeedParser sharedInstance]; instance.transportLayer = [self createSimpleOperationManager]; }); return instance; } + (AFHTTPRequestOperationManager *)createSimpleOperationManager { AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.responseSerializer = [[AFXMLParserResponseSerializer alloc] init]; manager.responseSerializer.acceptableContentTypes = [NSSet setWithArray:@[@"application/xml", @"text/xml",@"application/rss+xml"]]; return manager; } - (void)feedFromStringUrl:(NSString *)url success:(RSItemsBlock)success failure:(RSErrorBlock)failure { @weakify(self); [self.transportLayer GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { @strongify(self); NSDictionary *dom = [NSDictionary dictionaryWithXMLParser:responseObject]; NSArray *items = [self.parser itemFeed:dom]; success(items); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { failure(error); }]; } @end
RSFeedParser #import <Foundation/Foundation.h> @interface RSFeedParser : NSObject + (instancetype)sharedInstance; - (NSArray *)itemFeed:(NSDictionary *)dom; @end
#import "RSFeedParser.h" #import <MWFeedParser/NSDate+InternetDateTime.h> #import "RSFeedItem.h" NSString * const RSFeedParserChannel = @"channel"; NSString * const RSFeedParserItem = @"item"; NSString * const RSFeedParserTitle = @"title"; NSString * const RSFeedParserPubDate = @"pubDate"; NSString * const RSFeedParserDescription = @"description"; NSString * const RSFeedParserLink = @"link"; @implementation RSFeedParser + (instancetype)sharedInstance { static dispatch_once_t onceToken; static RSFeedParser *instance; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (NSArray *)itemFeed:(NSDictionary *)dom { NSDictionary *channel = dom[RSFeedParserChannel]; NSArray *items = channel[RSFeedParserItem]; return [items bk_map:^id(NSDictionary *item) { NSString *title = item[RSFeedParserTitle]; NSString *description = item[RSFeedParserDescription]; NSString *pubDateString = item[RSFeedParserPubDate]; NSString *linkString = item[RSFeedParserLink]; NSDate *pubDate = [NSDate dateFromInternetDateTimeString:pubDateString formatHint:DateFormatHintRFC822]; NSURL *link = [NSURL URLWithString:linkString]; return [RSFeedItem initWithTitle:title descriptionNews:description pubDate:pubDate link:link]; }]; } @end
RSFeedItem @interface RSFeedItem : NSObject @property (copy, nonatomic, readonly) NSString *title; @property (copy, nonatomic, readonly) NSString *descriptionNews; @property (strong, nonatomic, readonly) NSDate *pubDate; @property (strong, nonatomic, readonly) NSURL *link; + (instancetype)initWithTitle:(NSString *)title descriptionNews:(NSString *)descriptionNews pubDate:(NSDate *)pubDate link:(NSURL *)link; - (instancetype)initWithTitle:(NSString *)title descriptionNews:(NSString *)descriptionNews pubDate:(NSDate *)pubDate link:(NSURL *)link; @end
#import "RSFeedItem.h" @interface RSFeedItem () @property (copy, nonatomic, readwrite) NSString *title; @property (copy, nonatomic, readwrite) NSString *descriptionNews; @property (strong, nonatomic, readwrite) NSDate *pubDate; @property (strong, nonatomic, readwrite) NSURL *link; @end @implementation RSFeedItem + (instancetype)initWithTitle:(NSString *)title descriptionNews:(NSString *)descriptionNews pubDate:(NSDate *)pubDate link:(NSURL *)link { return [[self alloc] initWithTitle:title descriptionNews:descriptionNews pubDate:pubDate link:link]; } - (instancetype)initWithTitle:(NSString *)title descriptionNews:(NSString *)descriptionNews pubDate:(NSDate *)pubDate link:(NSURL *)link { self = [super init]; if (self != nil) { self.title = title; self.descriptionNews = descriptionNews; self.pubDate = pubDate; self.link = link; } return self; } @end
CoreDataはどこにありますか?
次に、システムの別の部分であるRSSリストの操作について検討します。
1)RSSリストを取得する
2)RSSを追加
3)RSSを削除する
4)アプリケーションを初めて起動すると、2つのRSSソースがあります。
最後のアイテムはどうですか? しかし、テストする必要があります...実際、それは絶対に難しくありません(
OCMockのおかげです )。
残りの3つのポイントはさらに興味深いものです。ここでは、
ReactiveCocoaが非常に役立ちます
。setUpメソッドで、MagicalRecordのモードを「インメモリ」に設定します。これにより、作業データの損傷について考える必要がなくなります。
また、4番目のポイントを実行するために、部分的な濡れを行います。
tearDownメソッドでは、MagicalRecordをクリーニングし、部分的なモーキングをクリーニングします。
RSLinkServiceIT.m setUp / tearDown @interface RSLinkServiceIT : RSTestCaseIT @property (strong, nonatomic) RSLinkService *service; @property (strong, nonatomic) id mockUserDefaults; @end @implementation RSLinkServiceIT - (void)setUp { [super setUp];
項目4を検証するテスト
testOnFirstRunHave2Link #pragma mark test - (void)testOnFirstRunHave2Link { OCMStub([self.mockUserDefaults boolForKey:RSHasBeenAddStandardLink]).andReturn(NO); [self asyncTest:^(XCTestExpectation *expectation) { @weakify(self); [self.service list:^(NSArray *items) { @strongify(self); [expectation fulfill]; XCTAssertEqual([items count], 2); } failure:^{ @strongify(self); [expectation fulfill]; XCTFail(@"error"); }]; } timeout:0.1]; }
そして今、最も興味深いのは、RSSリンクの追加/削除/受信を確認することです。
すべてがどのように連携するかを確認しましょう。 いくつかのリンクを追加し、リンクを削除して、それらのリストをリクエストします。 このサービスには非同期インターフェイスがあり(必要に応じてサーバーへの接続が簡単になります)、操作は互いに依存しています。 したがって、ReactiveCocoaを使用して同様のコードを処理します。
testList #pragma mark test - (void)testList { [self asyncTest:^(XCTestExpectation *expectation) { [self asyncTestList:expectation]; } timeout:0.1]; } - (void)asyncTestList:(XCTestExpectation *)expectation { NSString *rss1 = @"http://news.rambler.ru/rss/scitech1/"; NSString *rss2 = @"http://news.rambler.ru/rss/scitech2/"; RACSignal *signalAdd1 = [self createSignalAddRSS:rss1]; RACSignal *signalAdd2 = [self createSignalAddRSS:rss2]; RACSignal *signalRemove = [self createSignalRemove:rss1]; RACSignal *signalList = [self createSignalList]; [[[[signalAdd1 flattenMap:^RACStream *(id _) { return signalAdd2; }] flattenMap:^RACStream *(id _) { return signalRemove; }] flattenMap:^RACStream *(id _) { return signalList; }] subscribeNext:^(NSArray *items) { [expectation fulfill]; XCTAssertEqual([items count], 1); XCTAssertEqualObjects(items[0], rss2); } error:^(NSError *error) { [expectation fulfill]; XCTFail(@"%@", error); }]; } - (RACSignal *)createSignalAddRSS:(NSString *)rss { @weakify(self); return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); [self.service add:rss success:^{ [subscriber sendNext:nil]; [subscriber sendCompleted]; } failure:^(NSError *error) { @strongify(self); XCTFail(@"%@", error); }]; return nil; }]; } - (RACSignal *)createSignalRemove:(NSString *)rss { @weakify(self); return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); [self.service remove:rss success:^{ [subscriber sendNext:nil]; [subscriber sendCompleted]; } failure:^(NSError *error) { @strongify(self); XCTFail(@"%@", error); }]; return nil; }]; } - (RACSignal *)createSignalList { @weakify(self); return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); [self.service list:^(NSArray *items) { [subscriber sendNext:items]; [subscriber sendCompleted]; } failure:^{ [subscriber sendError:nil]; [subscriber sendCompleted]; }]; return nil; }]; }
残りのコード
RSLinkService #import <Foundation/Foundation.h> @interface RSLinkService : NSObject + (instancetype)sharedInstance; - (void)add:(NSString *)link success:(RSEmptyBlock)success failure:(RSErrorBlock)failure; - (void)list:(RSItemsBlock)callback failure:(RSEmptyBlock)failure; - (void)remove:(NSString *)link success:(RSEmptyBlock)success failure:(RSErrorBlock)failure; @end
#import "RSLinkService.h" #import "RSLinkDAO.h" @interface RSLinkService () @property (strong, nonatomic) RSLinkDAO *dao; @end @implementation RSLinkService + (instancetype)sharedInstance { static dispatch_once_t onceToken; static RSLinkService *instance; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; instance.dao = [RSLinkDAO sharedInstance]; }); return instance; } - (void)add:(NSString *)link success:(RSEmptyBlock)success failure:(RSErrorBlock)failure { [self.dao add:link]; success(); } - (void)list:(RSItemsBlock)callback failure:(RSEmptyBlock)failure { NSArray *list = [self.dao list]; callback(list); } - (void)remove:(NSString *)link success:(RSEmptyBlock)success failure:(RSErrorBlock)failure { [self.dao remove:link]; success(); } @end
RSLinkDAO #import <Foundation/Foundation.h> @interface RSLinkDAO : NSObject + (instancetype)sharedInstance; - (void)add:(NSString *)link; - (NSArray *)list; - (void)remove:(NSString *)link; @end
#import "RSLinkDAO.h" #import "RSLinkEntity.h" #import <MagicalRecord/MagicalRecord.h> #import "NSString+RS_RSS.h" @interface RSLinkDAO () @end @implementation RSLinkDAO + (instancetype)sharedInstance { static dispatch_once_t onceToken; static RSLinkDAO *instance; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (void)add:(NSString *)link { NSString *url = [link convertToBaseHttp]; RSLinkEntity *entity = [self linkToLinkEntity:url]; [entity.managedObjectContext MR_saveToPersistentStoreAndWait]; } - (NSArray *)list { NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults]; if (![standardUserDefaults boolForKey:RSHasBeenAddStandardLink]) { [self addStandartLink]; [standardUserDefaults setBool:YES forKey:RSHasBeenAddStandardLink]; [standardUserDefaults synchronize]; } NSArray *all = [RSLinkEntity MR_findAll]; return [self linkEntityToLink:all]; } - (void)addStandartLink { RSLinkEntity *entity = [self linkToLinkEntity:@"http://developer.apple.com/news/rss/news.rss"]; [entity.managedObjectContext MR_saveToPersistentStoreAndWait]; RSLinkEntity *entity1 = [self linkToLinkEntity:@"http://news.rambler.ru/rss/world"]; [entity1.managedObjectContext MR_saveToPersistentStoreAndWait]; } - (void)remove:(NSString *)link { RSLinkEntity *entity = [self entityWithLink:link]; [entity MR_deleteEntity]; [entity.managedObjectContext MR_saveToPersistentStoreAndWait]; } #pragma mark - convert - (NSArray *)linkEntityToLink:(NSArray *)entitys { return [entitys bk_map:^id(RSLinkEntity *entity) { return entity.link; }]; } - (RSLinkEntity *)linkToLinkEntity:(NSString *)link { RSLinkEntity *entity = [RSLinkEntity MR_createEntity]; entity.link = link; return entity; } - (RSLinkEntity *)entityWithLink:(NSString *)link { return [RSLinkEntity MR_findFirstByAttribute:@"link" withValue:link]; } @end
NSString + RS_RSS #import <Foundation/Foundation.h> @interface NSString (RS_RSS) - (instancetype)convertToBaseHttp; @end
#import "NSString+RS_RSS.h" @implementation NSString (RS_RSS) - (instancetype)convertToBaseHttp { NSRange rangeHttp = [self rangeOfString:@"http://"]; NSRange rangeHttps = [self rangeOfString:@"https://"]; if (rangeHttp.location != NSNotFound || rangeHttps.location != NSNotFound) { return self; } return [NSString stringWithFormat:@"http://%@", self]; } @end
すでに書かれているように、テストに集中するために余分なコードを削除しようとしました。
統合テストにより、システムの部品が正しく接続されているかどうかを確認できます。
ソースは
こちらです。