Introduction to RxTest - tasanobu tech blog

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

  • This post is based on

    • Xcode 8.1 / Swift 3.0
    • RxSwift 3.0.0
    • RxTest 3.0.0
  • The code shown in this post is published on my GitHub repository here .

What is RxTest?

  • RxTest is a test framework published at RxSwift repository.
  • Most(All?) of the unit testing in RxSwift is implemented with RxTest
  • RxTest is available for third-party like normal app developers and it’s easy to add it to your test target with Cocoapods by adding the code like below to your podfile.
1
2
3
4
5
target 'RxTest-SampleTests' do
pod 'RxTest', '~> 3.0'
pod 'RxSwift', '~> 3.0'
pod 'RxCocoa', '~> 3.0'
end

Main classes of RxTest

TestScheduler
  • is a scheduler in which virtual time is implemented.
  • emits events and executes arbitrary closures based on virtual time.
  • has factory methods to instantiate TestableObserver and TestObservable
TestableObservable
  • is an observable sequence which has events sent to observer at the designated virtual time and records subscriptions(subscribe/unsubscribe) during its lifetime
TestableObserver
  • is an observer which records all emitted events together with virtual time when they were received.

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.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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)
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

  • Introduction to RxTest - tasanobu tech blog