RxSwiftDay 8

RxTestを使ったUI層のテスト

More than 1 year has passed since last update.


はじめに

RxSwiftを最近触り始めたので、自分なりのUI層の設計とテストについて書きます。

RxSwiftのバージョンは3.0.1です。


基本的な設計

ui.001.jpeg


  • ViewController


    • UserEventの通知

    • UIの反映



  • Presenter


    • ViewControllerからUserEventを受け取る

    • ViewModelに取得したデータを反映

    • ViewControllerにUI反映の通知



  • ViewModel


    • 取得したデータを保持




実装


ViewController

// ViewController.swift

import UIKit
import RxSwift
import RxCocoa

class ViewControlller: UIViewController {

// MARK: - Outlet

@IBOutlet private weak var tableView: UITableView!

// MARK: - Property

var presenter: Presenter!
private let dataSource = DataSource()
private let disposeBag = DisposeBag()

// MARK: - LifeCycle

override func viewDidLoad() {
super.viewDidLoad()

setupUserEventBindings()
setupDrawingBindings()
}

// MARK: - Private

private func setupUserEventBindings() {
// 1. UserEvent
rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.map { _ in return () }
.shareReplay(1)
.bindTo(presenter.refreshTrigger)
.addDisposableTo(disposeBag)
}

private func setupDrawingBindings() {
// 5, 6. UIの反映
presenter.refreshTodosTrigger
.asDriver()
.drive(self.tableView.rx.items(dataSource: self.dataSource))
.addDisposableTo(disposeBag)
}
}


Presenter

// Presenter.swift

import UIKit
import RxSwift

protocol Input: class {
var refreshTrigger: PublishSubject<Void> { get }
}

protocol Output: class {
var refreshTodosTrigger: PublishSubject<[Todo]> { get }
}

class Presenter: Input, Output {

// MARK: - Input

private(set) var refreshTrigger = PublishSubject<Void>()

// MARK: - Output

private(set) var refreshTodosTrigger = PublishSubject<[Todo]>()

// MARK: - Property

private let useCase: UseCase

private var viewModel = ViewModel()
private let disposeBag = DisposeBag()

// MARK: - LifeCycle

init(useCase: UseCase) {
self.useCase = useCase

setupBindings()
}

// MARK: - Private

private func setupBindings() {
// 3. ViewModelに保持してるSubjectでEventを発行
// viewWillAppearが呼ばれたときにTodo一覧を取得するStream
refreshTrigger
.flatMap { [weak self] _ -> Observable<[Todo]> in
guard let weakSelf = self else {
return Observable<[Todo]>.error(PresentationError.caputureFailed)
}
return self.useCase.getAll()
}
.subscribe(
onNext: { [weak self] todos in
guard let weakSelf = self else {
return
}
self.viewModel.todos.onNext(todos)
},
onError: { error in
print(error)
}
)
.addDisposableTo(disposeBag)

// 4. PresenterにBinding
// Todo一覧を取得した際にTableViewに反映するStream
viewModel.todos
.bindTo(refreshTodosTrigger)
.addDisposableTo(disposeBag)
}
}


ViewModel

// ViewModel.swift

import Foundation
import RxSwift

struct ViewModel {
var todos = PublishSubject<[Todo]>()
}


UI層のテスト

いよいよ、メインのテストの話です。

個人的にはUIのテストはUnit Testで保証するのはかなり難しいと考えているので、E2Eテストなどで保証すればいいと考えてます。ですので、ここで取り上げるUI層のテストは主にPresenterのテストになります。

PresenterはProtocolでInput, Outputを定義しているので、Unit TestはInputとOutputの組み合わせを網羅するように書きます。今回はviewWillAppearのタイミングでtableViewにデータを反映するだけですので1組み合わせだけですが...

TestにはQuick, Nimble, RxTestを用います。RxTestのTestSchedulerを用いて、想定するInputを発生させます。

// PresenterSpec.swift

import Quick
import Nimble
import RxSwift
import RxTest

class PresenterSpec: QuickSpec {

override func spec() {
describe("Presenter") {
var presenter: Presenter!
var scheduler: TestScheduler!

beforeEach {
presenter = Mock.container.resolve(Presenter.self)
}

context("when will appear") {
// API RequestをMockしたUseCase
class MockUseCaseImpl: UseCaseImpl {
var xs: TestableObservable<[Todo]>
init(repository: Repository, scheduler: TestScheduler) {
// Subscribeされてから100経過後にonNextが出力される
self.xs = scheduler.createColdObservable([
next(100,
[
Todo(title: "RxSwift"),
Todo(title: "Swinject"),
Todo(title: "Quick"),
Todo(title: "Nimble")
])
])
super.init(repository: repository)
}

override func getAll() -> Observable<[Todo]> {
// 返り値をMockしている
return xs.asObservable()
}
}

beforeEach {
scheduler = TestScheduler(initialClock: 0)
// MockしているUseCaseをInjectしている
presenter = Presenter(useCase: MockUseCaseImpl(repository: RepositoryImpl(dataStore: DataStore()), scheduler: scheduler))
}

it("show todo items in tablew view") {
let results = scheduler.createObserver([Todo].self)
let disposeBag = DisposeBag()

scheduler.scheduleAt(100) {
// useCase.getAll()が呼ばれる
presenter.refreshTrigger.onNext()
}

scheduler.scheduleAt(200) {
// schedulerで設定したものが呼ばれるのでSubscribeする
presenter.refreshTodosTrigger
.subscribe(results)
.addDisposableTo(disposeBag)
}

scheduler.start()

expect(results.events.count).to(equal(1))
expect(results.events[0].time).to(equal(200))
expect(results.events[0].value.element?.count).to(equal(4))
expect(results.events[0].value.element?[0].title).to(equal("RxSwift"))
expect(results.events[0].value.element?[1].title).to(equal("Swinject"))
expect(results.events[0].value.element?[2].title).to(equal("Quick"))
expect(results.events[0].value.element?[3].title).to(equal("Nimble"))
}
}
}
}
}


まとめ

RxSwiftを触ってみて自分なりに色々と考えてみましたが、まだまだ理解が足りてないないなといった感じがします。デモのプロジェクトには表示だけではなく、表示・追加・編集・削除の機能を実装してますので、もう少し複雑ですので興味を持っていただいたら見てみてください。

https://github.com/Nonchalant/Rx_Demo