Skip to content

Introduction to RxTest - tasanobu tech blog

轩辕十四
Published date:

I had a presentation (in Japanese) about RxTest at RxSwift Meetup held at Tokyo. There does not seem to be lots of RxTest related materials on the web so I decided to translate the presentation into English and to write this post.

Notes

What is RxTest?

target 'RxTest-SampleTests' do
    pod 'RxTest',     '~> 3.0'
    pod 'RxSwift',    '~> 3.0'
    pod 'RxCocoa',    '~> 3.0'
end

Main classes of RxTest

TestScheduler
TestableObservable
TestableObserver

Samples

Let’s see how to use the classes introduced above.

map operator

The following is the test code of map operetor in RxSwift (I simplified a little.)

func test_map() {
    // 1. Instantiate TestScheduler by designating virtual time 0
    let scheduler = TestScheduler(initialClock: 0)
    // 2. Instantiate TestableObservable<Int>
    // by designating value together with virtual time.
    let observable = scheduler.createHotObservable([
        next(150, 1),  // (virtual time, value)
        next(210, 0),
        next(240, 4),
        completed(300)
        ])
    // 3. Instantiate TestableObserver<Int>
    let observer = scheduler.createObserver(Int.self)
    // 4. Make `observer` subscribe `observable` at virtual time 200
    scheduler.scheduleAt(200) {
        observable.map { $0 * 2 }
            .subscribe(observer)
            .addDisposableTo(self.disposeBag)
    }
    // 5. Start `scheduler`
    scheduler.start()
    let expectedEvents = [
        next(210, 0 * 2),
        next(240, 4 * 2),
        completed(300)
    ]
    // 6-1. Compare the events which `observer` received
    XCTAssertEqual(observer.events, expectedEvents)
    let expectedSubscriptions = [
        Subscription(200, 300)
    ]
    // 6-2. Compare the virtual times when `observable` was subscribed/unsubscribed
    XCTAssertEqual(observable.subscriptions, expectedSubscriptions)
}

Table of contents

Open Table of contents

ViewModel of Github follower search view

As a more practical example, let’s see the view model for Github follower search view.

ViewModel

The following class is the view model for the view and it is the test target.

class SearchViewModel {
    let disposeBag = DisposeBag()
    // api call status: .loading, .loaded, .error
    var state: Variable<State>  = Variable(.loaded([]))
    // followers list
    var users: Variable<[User]> = Variable([])
    // Client which has a web API logic
    let client: Client
    init(client: Client) {
        self.client = client
    }
    // Action method to search followers
    func searchFollowers(ofUser user: String) {
        state.value = .loading
        client.fetchFollowers(ofUser: user)
            .subscribe { event in
                switch event {
                case .next(let users) :
                    self.state.value = .loaded(users)
                    self.users.value = users
                case .error(let e):
                    self.state.value = .error(e as? Client.Error ?? .unknown)
                    self.users.value = []
                default: // ignore `.completed`
                    break
                }
            }
            .addDisposableTo(disposeBag)
    }
}

There are 2 important points from unit testing perspective.

  1. Pass a Client object to SearchModelView ‘s initializer
  2. Call Client.fetchFollowers() inside SearchViewModel

Mock Client

The following class is the mock object of Client which is used for unit testing.

The mock class inherits Client (in order to simplify the sample). It has a property response which is a TestableObservable used as API response and overrides fetchFollowers() and returns response from it.

class MockClient: Client {
    // Observable used as API response
    let response: TestableObservable<[User]>
    init(response: TestableObservable<[User]>) {
        self.response = response
        super.init()
    }
    override func fetchFollowers(ofUser user: String) -> Observable<[User]> {
        return response.asObservable()
    }
}

Test code: SearchViewModel.state

Below is the test sample of SearchViewModel.state. The value of the property changes in response to searchFollowers() which causes network request.

The overall flow is almost the same as the previous example of map operator.

func test_state_when_searchFollowers_succeeded() {
    let users = [User(id: 12091114, name: "tunepolo")]
    // 1. Instantiate TestableObserver
    let observer = scheduler.createObserver(State.self)
    // 2. Instantiate TestableObservable
    let observable = scheduler.createColdObservable([
        next(100, users)
        ])
    // 3. Instantiate MockClient with the observable
    let client = MockClient(response: observable)
    // 4. Instantiate SearchViewModel with MockClient
    let viewModel = SearchViewModel(client: client)
    // 5. Make `observer` subscribe `observable` at virtual time 100
    scheduler.scheduleAt(100) {
      viewModel.state.asObservable()
          .subscribe(observer)
          .addDisposableTo(self.disposeBag)
    }        
    // 6. Call `viewModel.searchFollowers()` at virtual time 200
    scheduler.scheduleAt(200) {
        viewModel.searchFollowers(ofUser: "tasanobu")
    }
    // 7. Start `scheduler`
    scheduler.start()
    // 8. Inspect the events that the observer received
    let expectedEvents = [
        next(100, State.loaded([])),
        next(200, State.loading),
        next(300, State.loaded(users))
    ]
    XCTAssertEqual(observer.events, expectedEvents)
}

Wrap up

This post describes the overview of RxTest and the examples of unit testing with RxTest. You might think it’s hard to get into unit testing of features implemented with RxSwift. However, it’s doable to write such test code by following the steps below.

  1. Instantiate TestScheduler
  2. As TestableObservable, instantiate an event sequence which you want to inspect
  3. Instantiate TestableObserver which records events to be inspected
  4. Make TestableObserver subscribe TestableObservable
  5. Start TestScheduler
  6. Inspect TestableObserver.events or TestableObservable.subscriptions depending on your test needs.

From:

Previous
Python 学习笔记(三)【基础补全】
Next
重装系统提示没有 Git 的解决方式