0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CombineLatest Streamのテスト手法

Posted at

はじめに

テスト書いてますか?
テストコードが0のアプリにようやくテストコードを入れるようになり、RxTest/RxBlockingを使いはじめました。
CombineLatestで複数のObservabelをまとめて観測する値がtuple型のときに、テストで詰まったので解決策を残します。

環境

  • macOS Catalina 10.15.3
  • Xcode 11.3.1 (11C504)
  • RxSwift 5.0.1

Rxのテスト

Model

テストしたいModelを用意します。
今回はuserStream: Observable<(String, Int)>の値をテストします。


import RxSwift
import RxCocoa

class UserModel {

    let name: BehaviorRelay<String> = BehaviorRelay(value: "")
    let age: BehaviorRelay<Int> = BehaviorRelay(value: 0)

    // outputStream
    let userStream: Observable<(String, Int)>

    init(name: String, age: Int) {
        self.name.accept(name)
        self.age.accept(age)

        userStream = Observable.combineLatest(self.name, self.age)
    }
}

テストコード

それではUserModelのuserStreamのテストを書いてみます。


import XCTest
import RxSwift
import RxCocoa
import RxBlocking
import RxTest
@testable import TestApp

class TestAppTests: XCTestCase {

    var userModel: UserModel!
    var scheduler: TestScheduler!
    var disposeBag: DisposeBag!

    override func setUp() {
        userModel = UserModel(name: "takutoki", age: 5)
        scheduler = TestScheduler(initialClock: 0)
        disposeBag = DisposeBag()
        super.setUp()
    }

    func testObservableWithTuple() {
        // assertする型を定義
        let testObserver = scheduler.createObserver((String, Int).self)

        // テスト対象のbind先をtestObserverにする
        userModel.userStream
            .bind(to: testObserver)
            .disposed(by: disposeBag)

        // name に流すテストObservable
        let nameTestObservable = scheduler.createColdObservable([
            Recorded.next(10, "ShirasakaKoume"),
            Recorded.next(20, "KoshimizuSachiko"),
            Recorded.next(30, "HoshiSyoko")
            ])

        // age に流すテストObservable
        let ageTestObservable = scheduler.createColdObservable([
            Recorded.next(15, 13),
            Recorded.next(25, 14),
            Recorded.next(35, 15)
            ])

        nameTestObservable
            .bind(to: userModel.name)
            .disposed(by: disposeBag)

        ageTestObservable
            .bind(to: userModel.age)
            .disposed(by: disposeBag)
        
        // expect
        // 0: 初期値
        // 10,20,30: nameが変更されたときに流れる
        // 15,25,35: ageが変更されたときに流れる

        let expected: [Recorded<Event<(String, Int)>>] = [
            Recorded.next(0, ("takutoki", 5)),
            Recorded.next(10, ("ShirasakaKoume", 5)),
            Recorded.next(15, ("ShirasakaKoume", 13)),
            Recorded.next(20, ("KoshimizuSachiko", 13)),
            Recorded.next(25, ("KoshimizuSachiko", 14)),
            Recorded.next(30, ("HoshiSyoko", 14)),
            Recorded.next(35, ("HoshiSyoko", 15))
        ]

        scheduler.start()

        XCTAssertEqual(testObserver.events, expected) // Global function 'XCTAssertEqual(_:_:file:line:)' requires that '(String, Int)' conform to 'Equatable'
    }
}

XCTAssertEqualの行でコンパイル前にエラーが起きます。

余談ですが、Xcode10.3ではビルド時に以下のエラーが発生します。
これだと原因がいまいちはっきりしませんね。
実際の直面したときは以下のエラーだったので、解決法がなかなか見つかりませんでした。

Expression type '()' is ambiguous without more context

どうするか

エラーメッセージにある通りEquatableに準拠した型同士でassertするようにすれば良いです。
そこでstruct同士でassertを行うようにします。

struct AssertStruct: Equatable {
    
    var name: String
    var age: Int
    
    public static func == (lhs: AssertStruct, rhs: AssertStruct) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

Equatableを準拠したAssertStructを作成して ==メソッドを実装します。
テストも書き直します。


func testObservableWithTuple() {
        // assertする型をAssertStructに変更する
        let testObserver = scheduler.createObserver(AssertStruct.self)

        // userStreamをAssertStructに変換するoperatorを追加する
        userModel.userStream
            .map{ AssertStruct(name: $0.0, age: $0.1) }
            .bind(to: testObserver)
            .disposed(by: disposeBag)

        let nameTestObservable = scheduler.createColdObservable([
            Recorded.next(10, "ShirasakaKoume"),
            Recorded.next(20, "KoshimizuSachiko"),
            Recorded.next(30, "HoshiSyoko")
            ])

        let ageTestObservable = scheduler.createColdObservable([
            Recorded.next(15, 13),
            Recorded.next(25, 14),
            Recorded.next(35, 15)
            ])

        nameTestObservable
            .bind(to: userModel.name)
            .disposed(by: disposeBag)

        ageTestObservable
            .bind(to: userModel.age)
            .disposed(by: disposeBag)

        // 期待する値はAssertStructの型に変更する
        let expected: [Recorded<Event<AssertStruct>>] = [
            Recorded.next(0, AssertStruct(name: "takutoki", age: 5)),
            Recorded.next(10, AssertStruct(name: "ShirasakaKoume", age: 5)),
            Recorded.next(15, AssertStruct(name: "ShirasakaKoume", age: 13)),
            Recorded.next(20, AssertStruct(name: "KoshimizuSachiko", age: 13)),
            Recorded.next(25, AssertStruct(name: "KoshimizuSachiko", age: 14)),
            Recorded.next(30, AssertStruct(name: "HoshiSyoko", age: 14)),
            Recorded.next(35, AssertStruct(name: "HoshiSyoko", age: 15))
        ]

        scheduler.start()

        XCTAssertEqual(testObserver.events, expected)
    }
スクリーンショット 2020-02-08 15.24.36.png All Greeeeen!!!

最後に

上記の例ではこんなんテストする必要あるんか?と思いますが、
実際の業務ではoperatorを複数噛ませたりdistinctUntilChangedなど入れたりと複雑な要件になりがちです。
テストでoutputを素早く確認できるのは安心ですね。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?