反応性のテスト-RxSwiftの単体テストの作成方法

前の記事でRxSwiftフレームワークを使用して単純なリアクティブiOSアプリケーションを作成する方法について説明しました。 コメントの1つで、リアクティブコードの単体テストの記述について話すように要求されました。 RxSwiftに関する他の記事をいくつか読んだ後、それについて書くことを約束しました。 しかし、私は先に進みました-私がこれから書くことは、 この記事とこの記事で美しく明らかにされました。 私は個人的に彼の巨匠の仕事に感謝したい-これらの2つの記事は私のお気に入りにしっかりと定着しており、私の仕事を助けてくれます。



さて、テストの作成を開始します!



機能を追加する



テストを書き始める前に、Facebookをもう少し苦しめ、壁に投稿を作成する機能を書きましょう。 これを行うには、まずLoginViewController.viewDidLoad()のログインボタンにpublish_actions権限を追加する必要があります。

loginButton.publishPermissions = ["publish_actions"]
      
      





その後、APIManagerファイルに投稿を作成するリクエストを作成します。

 func addFeed(feedMessage: String) -> Observable<Any> { return Observable.create { observer in let parameters = ["message": feedMessage] let addFeedRequest = FBSDKGraphRequest.init(graphPath: "me/feed", parameters: parameters, HTTPMethod: "POST") addFeedRequest.startWithCompletionHandler { (connection, result, error) -> Void in if error != nil { observer.on(.Error(error!)) } else { observer.on(.Next(result)) observer.on(.Completed) } } return AnonymousDisposable { } } }
      
      







次に、メッセージを入力するためのUITextViewとメッセージを送信するためのUIButtonの2つの要素を持つ新しい画面を作成します。 この部分については説明しませんが、すべてがかなり標準的であり、困難を抱えています。この記事の最後で、Githubへのリンクを見つけて実装を確認できます。



次に、新しい画面のViewModelを作成する必要があります。

AddPostViewModelの実装
 class AddPostViewModel { let validatedText: Observable<Bool> let sendEnabled: Observable<Bool> // If some process in progress let indicator: Observable<Bool> // Has feed send in let sendedIn: Observable<Any> init(input: ( feedText: Observable<String>, sendButton: Observable<Void> ), dependency: ( API: APIManager, wireframe: Wireframe ) ) { let API = dependency.API let wireframe = dependency.wireframe let indicator = ViewIndicator() self.indicator = indicator.asObservable() validatedText = input.feedText .map { text in return text.characters.count > 0 } .shareReplay(1) sendedIn = input.sendButton.withLatestFrom(input.feedText) .flatMap { feedText -> Observable<Any> in return API.addFeed(feedText).trackView(indicator) } .catchError { error in return wireframe.promptFor((error as NSError).localizedDescription, cancelAction: "OK", actions: []) .map { _ in return error } .flatMap { error -> Observable<Any> in return Observable.error(error) } } .retry() .shareReplay(1) sendEnabled = Observable.combineLatest( validatedText, indicator.asObservable() ) { text, sendingIn in text && !sendingIn } .distinctUntilChanged() .shareReplay(1) } }
      
      







入力ブロックを見てみましょう。入力にfeedText(ニュースのテキスト)とsendButton(ボタンをクリックするイベント)を入力します。 クラス変数には、validatedText(テキストフィールドが空ではないことを確認する)、sendEnabled(投稿ボタンが使用可能であることを確認する)、およびsendedIn(投稿を送信する要求を満たす)があります。 validatedText変数を詳しく見てみましょう。

 validatedText = input.feedText .map { text in return text.characters.count > 0 } .shareReplay(1)
      
      





ここではすべてが非常に簡単です-入力に送信したテキストを取得し、その中の文字数を確認します。 文字がある場合はtrue、そうでない場合はfalseが返されます。 次に、sendEnabled変数を検討します。

 sendEnabled = Observable.combineLatest( validatedText, indicator.asObservable() ) { text, sendingIn in text && !sendingIn } .distinctUntilChanged() .shareReplay(1)
      
      





ここでも、すべてが簡単です。 テキストとダウンロードインジケータの最新のステータスを取得します。 テキストが空でなく、ロードがない場合はtrueを返し、そうでない場合はfalseを返します。 sendedInフィールドの処理は残ります。

 sendedIn = input.sendButton.withLatestFrom(input.feedText) .flatMap { feedText -> Observable<Any> in return API.addFeed(feedText).trackView(indicator) } .catchError { error in return wireframe.promptFor((error as NSError).localizedDescription, cancelAction: "OK", actions: []) .map { _ in return error } .flatMap { error -> Observable<Any> in return Observable.error(error) } } .retry() .shareReplay(1)
      
      





複雑なことは何もありません。 input.feedTextから最新の値を取得し、エラーをキャッチした場合、投稿を送信するリクエストを実行しようとします-エラーを処理し、ユーザーに表示し、再試行()して、クリックイベントから切り離されないようにします。



スーパー、ViewModelを終了し、ポスト追加コントローラーに移動して、そこに次のコードを記述します。

 let viewModel = AddPostViewModel( input: ( feedText: feedTextView.rx_text.asObservable(), sendButton: sendFeed.rx_tap.asObservable() ), dependency: ( API: APIManager.sharedAPI, wireframe: DefaultWireframe.sharedInstance ) ) let progress = MBProgressHUD() progress.mode = MBProgressHUDMode.Indeterminate progress.labelText = " ..." progress.dimBackground = true viewModel.indicator.asObservable() .bindTo(progress.rx_mbprogresshud_animating) .addDisposableTo(disposeBag) viewModel.sendEnabled .subscribeNext { [weak self] valid in self!.sendFeed.enabled = valid self!.sendFeed.alpha = valid ? 1.0 : 0.5 } .addDisposableTo(self.disposeBag) viewModel.sendedIn .flatMap { _ -> Observable<String> in return DefaultWireframe.sharedInstance.promptFor("   !", cancelAction: "OK", actions: []) .flatMap { action -> Observable<Any> in return Observable.just(action) } } .subscribeNext { action in self.navigationController?.popToRootViewControllerAnimated(true) } .addDisposableTo(self.disposeBag)
      
      





AddPostViewModelクラスのオブジェクトを作成し、ボタンを使用してsendEnabled変数を設定し、追加された投稿のステータスを追跡するsendedIn変数を使用して、成功した場合、ユーザーについて表示し、メイン画面に戻ります。 すべてが機能することを確認し、最後にテストに進みます。



RxSwiftを使用する場合の単体テストの概念



イベントを記録するという概念から始めましょう。 次のように、イベントの配列を定義しましょう。

 let booleans = ["f": false, "t": true]
      
      





タイムライン形式でこれを想像してください:

 --f-----t---
      
      





最初にタイムラインでfalseイベントを発生させ、次にtrueイベントを発生させました。

次の行はShedulerオブジェクトです。 タイムラインをイベントの配列に変換できます。たとえば、次のように上記のタイムラインを変換します。

 [shedule onNext(false) @ 0.4s, shedule onNext(true) @ 1.6s]
      
      





さらに、Shedulerでは、シーケンスイベントを同じ形式で記録できます。 多くの機能がありますが、当面はこれら2つで十分です。



それでは、テストの概念に移りましょう。 以下で構成されています。最初に設定した予想されるイベント(予想される)と、ViewModelで実際に発生する(記録される)実際のイベントがあります。 まず、予想されるイベントをタイムラインに記録し、Shedulerオブジェクトを使用してそれらを配列に変換します。次に、テストされたViewModelを取得し、Shedulerオブジェクトを使用してすべてのイベントを配列に書き込みます。



その後、予想されるイベントの配列を記録されたイベントと比較し、ViewModelが期待どおりに機能するかどうかを判断できます。 厳密に言えば、イベントだけでなくその数も比較できます。FeedsViewModelの単体テストを見つけることができるプロジェクトソースコードでは、テーブルセルのクリック数が比較されます。



私の実践が示すように、ビジネスロジックをテストするには、ViewModelをテストでカバーするだけで十分ですが、これは議論の余地のある問題であり、喜んで議論します。



テストを開始



最初に、AddPostViewModelをテストします。 まず、Podfileを構成する必要があります。

 target 'ReactiveAppTests' do pod 'RxTests', '~> 2.0' pod 'FBSDKLoginKit' pod 'RxCocoa', '~> 2.0' end
      
      





次に、 pod installコマンドを実行し、完了するのを待って、ワークスペースを開きます。 テスト用のモックアップを作成しましょう。 RxSwiftリポジトリから、 WireframeをテストするためのモックアップNotImplementedStubsを取得します。 APIのモーションキャプチャは次のようになります。

 class MockAPI : API { let _getFeeds: () -> Observable<GetFeedsResponse> let _getFeedInfo: (String) -> Observable<GetFeedInfoResponse> let _addFeed: (String) -> Observable<AnyObject> init( getFeeds: () -> Observable<GetFeedsResponse> = notImplemented(), getFeedInfo: (String) -> Observable<GetFeedInfoResponse> = notImplemented(), addFeed: (String) -> Observable<Any> = notImplemented() ) { _getFeeds = getFeeds _getFeedInfo = getFeedInfo _addFeed = addFeed } func getFeeds() -> Observable<GetFeedsResponse> { return _getFeeds() } func getFeedInfo(feedId: String) -> Observable<GetFeedInfoResponse> { return _getFeedInfo(feedId) } func addFeed(feedMessage: String) -> Observable<AnyObject> { return _addFeed(feedMessage) } }
      
      





MockAPIオブジェクトを簡単に作成できるように、テストクラス用の小さなヘルパー拡張機能を作成します。

 extension ReactiveAppTests { func mockAPI(scheduler: TestScheduler) -> API { return MockAPI( getFeeds: scheduler.mock(feeds, errors: errors) { _ -> String in return "--fs" }, getFeedInfo: scheduler.mock(feedInfo, errors: errors) { _ -> String in return "--fi" }, addFeed: scheduler.mock(textValues, errors: errors) { _ -> String in return "--ft" } ) } }
      
      







次に、予想されるイベントのチェーン(予想)を作成する必要があります。 プログラムの動作を示す必要があります。 これを行うには、[String:YOUR_TYPE]という形式の一連の配列を作成する必要があります。Stringは変数の名前、YOUR_TYPEは変数が呼び出されたときに返されるデータのタイプです。 たとえば、ブール変数の配列を作成します。

 let booleans = ["t" : true, "f" : false]
      
      





なぜこれがすべて必要なのかまだ明らかではないので、テスト用に残りのアレイを作成し、どのように機能するかを見てみましょう。

 //    let events = ["x" : ()] //    let errors = [ "#1" : NSError(domain: "Some unknown error maybe", code: -1, userInfo: nil), ] //       let textValues = [ "ft" : "feed", "e" : "" ] //   // ,      ,        :-) let feeds = [ "fs" : GetFeedsResponse() ] let feedInfo = [ "fi" : GetFeedInfoResponse() ] let feedArray = [ "fa" : [Feed]() ] let feed = [ "f" : Feed(createdTime: "1", feedId: "1") ]
      
      





次に、予想されるイベントのチェーンを作成します。

 let ( feedTextEvents, buttonTapEvents, expectedValidatedTextEvents, expectedSendFeedEnabledEvents ) = ( scheduler.parseEventsAndTimes("e----------ft------", values: textValues).first!, scheduler.parseEventsAndTimes("-----------------x-", values: events).first!, scheduler.parseEventsAndTimes("f----------t-------", values: booleans).first!, scheduler.parseEventsAndTimes("f----------t-------", values: booleans).first! )
      
      





それでは、この問題に対処しましょう。 ご覧のとおり、4つの変数(feedTextEvents、buttonTapEvents、expectedValidatedTextEvents、expectedSendFeedEnabledEvents)のイベントを記録します。 最初の変数はfeedTextEvents、そのイベントチェーンはscheduler.parseEventsAndTimes(「e ---------- ft ------」、値:textValues).first!.. textValuesからイベントを取得し、変数は2つのみです: "e": ""-空の文字列、 "ft": "feed-値を持つ文字列" feed "。 それでは、イベントチェーンe ---------- ft ------を見てみましょう。まず、イベントチェーン内でイベントeを呼び出します。これにより、現時点では空の文字列があり、ある時点でflイベントと呼びます。つまり、変数に「フィード」という単語を書き込んだと言います。



次に、expectedValidatedTextEventsなど、残りの変数を見てみましょう。 feedTextEventsに空の文字列がある場合、expectedValidatedTextEventsはfalseになります。 ブール配列を見て、fがfalseであることを確認します。したがって、feedTextEventsのeイベントを呼び出すときは、expectedValidatedTextEventsのfイベントを呼び出す必要があります。 feedTextEvents変数に対してftイベントが発生すると、つまり、テキストフィールドのテキストが空にならない限り、tイベントが発生するはずです-expectedValidatedTextEventsの場合はtrueです。



expectedSendFeedEnabledEventsでも同じことが言えます。テキストフィールドが空にならない場合、ボタンが有効になり、イベントtを発生させる必要があります-trueです。 さて、buttonTapEvents変数では、ボタンが使用可能になった後にボタンクリックイベントを発生させます。



これは、RxSwiftの単体テストのキーポイントです。イベントのチェーンを作成する方法を理解し、適切なタイミングでイベントが正しく呼び出されるようにイベントを配置する方法を学習します。 たとえば、feedTextEvents変数のftイベントが発生する前にexpectedValidatedTextEvents変数でt-trueイベントを発生させようとすると、expectedValidatedTextEventsイベントはtrue行が空の状態では発生しないため、テストは失敗します。 一般に、何が何であるかを自分で理解するために一連のイベントをいじってみることをお勧めします。次に、コードを追加しましょう。

 let wireframe = MockWireframe() let viewModel = AddPostViewModel( input: ( feedText: scheduler.createHotObservable(feedTextEvents).asObservable(), sendButton: scheduler.createHotObservable(buttonTapEvents).asObservable() ), dependency: ( API: mock, wireframe: wireframe ) ) // run experiment let recordedSendFeedEnabled = scheduler.record(viewModel.sendEnabled) let recordedValidatedTextEvents = scheduler.record(viewModel.validatedText) scheduler.start() // validate XCTAssertEqual(recordedValidatedTextEvents.events, expectedValidatedTextEvents) XCTAssertEqual(recordedSendFeedEnabled.events, expectedSendFeedEnabledEvents)
      
      







テストを実行し、緑色に点灯しているというこの心地よい感覚を体験します:-)同じ原則を使用して、FeedsViewModelの単体テストを作成しました。これはプロジェクトリポジトリで確認できます。 それはすべて私のためです、私はコメント/提案/願いに喜んでいます、あなたの注意をありがとう!



All Articles