Testing Asynchronicity in Swift & Kotlin

Eric Silverberg
ITNEXT
Published in
3 min readDec 7, 2020

--

Comparing Combine, ReactiveSwift and RxJava for tests

Multiple threads are beautiful but tricky!

In this series we introduced a Clean MVVM architecture for writing consistent code in Swift and Kotlin.

Central to our Clean MVVM architecture are reactive streams, which implicitly introduce asynchronicity into our app. Testing reactive streams is not straightforward — when writing tests that use Reactive streams, we must find a way to either wait or block until our streams have completed or data has been emitted.

Reactive programming

Move execution to the main thread

On Android, we can override the thread on which reactive streams are invoked, so that normally asynchronous calls are made synchronous. We do this with two beforeEach methods in an ExtendWith block.

@ExtendWith(InstantExecutorExtension::class,         
RxTestSchedulersExtension::class

The InstantExecutorExtension applies to LiveData, and is adapted from this blog post. The RxTestSchedulersExtension applies to thread execution in RxJava. Both implementations can be found below:

Evaluating streams with TestObserver

Reactive streams need tests, and for this, Combine, ReactiveSwift, and RxJava have the concept of a TestObserver. A TestObserver is a class that is given a stream, such as a Publisher (Combine), SignalProducer (ReactiveSwift) or Observable (RxJava), for which it keeps a history of the emitted values. You also may use a TestObserver to wait for a given event to be emitted before a test can proceed.

Note that we typically declare TestObserver objects as a shared object in each inner class/context, we assign them in the beforeEach blocks, and then we interrogate them in each of the then tests.

Combine

To evaluate streams in Combine, use the CombineExpectations package. This package defines a Recorder object, which is functionally equivalent to a TestObserver on Android.

let eventsRecorder = context.viewModel.events.record()
context.viewModel.onEditProfileClick()
let event = try eventsRecorder.next().get()
XCTAssertEqual(event, PSSAccountViewModel.Event.triggerProfileOfflineAlert)

ReactiveSwift

ReactiveSwift does not natively ship with a TestObserver, but Kickstarter has helpfully defined such a class in their repository. Here is an example of observing that a .flagProfile event gets emitted:

RxJava

To evaluate streams in RxJava, we use the TestObserver class which is defined as part of the io.reactivex.observers package. Here is an example of observing that a FlagProfile event get emitted.

LiveData

Our use of LiveData is generally as a substitute for BehaviorSubject at the View layer. As a result, if we have applied the InstantExecutor extension described above, we can generally just access the .value property directly off of the main thread in a test and do not require any special handling or observers.

More in the series

Other series you might like

Clean API Architecture (2021)
Classes, execution patterns, and abstractions when building a modern API endpoint.

Android Activity Lifecycle considered harmful (2021)
Android process death, unexplainable NullPointerExceptions, and the MVVM lifecycle you need right now

About the author

Eric Silverberg is the CEO and founder of Perry Street Software, publisher of two of the world’s largest LGBTQ+ dating apps on iOS and Android.

--

--