RSSリーダーの一部の例を使用したObjective-C統合テスト

以前の記事では、単体テストについて検討しましたが、今回は統合テストについて説明します。

サンプルが大きくなりすぎず、素材も含まれているため、 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]; // Put setup code here. This method is called before the invocation of each test method in the class. self.service = [RSFeedService sharedInstance]; self.url = @"http://images.apple.com/main/rss/hotnews/hotnews.rss"; }
      
      







テスト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]; // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; }
      
      





また、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]; // Put setup code here. This method is called before the invocation of each test method in the class. [MagicalRecord setupCoreDataStackWithInMemoryStore]; self.service = [RSLinkService sharedInstance]; id userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setBool:YES forKey:RSHasBeenAddStandardLink]; self.mockUserDefaults = OCMPartialMock(userDefaults); } - (void)tearDown { [MagicalRecord cleanUp]; [self.mockUserDefaults stopMocking]; // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; }
      
      







項目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
      
      









すでに書かれているように、テストに集中するために余分なコードを削除しようとしました。

統合テストにより、システムの部品が正しく接続されているかどうかを確認できます。

ソースはこちらです。



All Articles