The quickie of Alexandre Delattre (Viseo) on Marble testing with Rx (JS/Java/…) during the DevFest Toulouse 2017 was particularly interesting.
What is Rx?
Rx is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections.
- From RxJS doc
We can use Rx in the frontend (for service calls combinations and reactive user interface) as well as in the backend (micro-services calls combinations, web sockets, …).
Issues
The current trend is to transform imperative programming into reactive functional programming. With the tools at our disposal, testing asynchronous behaviours is hard, and often, developers just skip this important step. But it is possible! And now, simpler than ever. So how to do that? How to check that our streams unfold the way we want them to?
You guessed right: with Marble Testing.
Marble diagrams
In order to represent Observables, we define Marble diagrams. They are drawn as a horizontal timeline, with events occurring as visual nodes. We can represent them like this example of a the merge function that takes two observables and return a merge of the two.
You can refer to RxMarbles website in order to find interactive diagrams of Rx Observables. In order to use them in code, we define an ASCII notation.
First, we define the time frame (default is 10ms). Then we can have a look at the different symbols that we need:
- : Nothing happens during one frame
| : the observable is completed (onComplete)
# : observable error (onError)
x : the observable emits a value (onNext)
^ : subscription point of an Observable (only for hot Observables)
() : value grouping
For this example of application, the speaker chose the language Kotlin, but we could do the same with any Rx supported language and platform (see full list onReactiveX site).
Application Requirements
We have an “instant search” application, with the user inputting their city’s name. After a 500ms delay, we launch the search, and a loading progress is visible to the user during the search. Then the result is displayed, or an error, if need be.
Interfaces
Our available interfaces are the following:
interface WeatherViewModel {
// Inputs
val city: Subject
// Outputs
val state: Observable<State>
val weather: Observable<WeatherData>
}
sealed class State
object Idle : State()
object Loading : State()
data class Error(val e:Throwable) : State()
data class WeatherData (
val city: String,
val pictoUrl: String,
val minTemperature: Float,
val maxTemperature: Float
)
interface WeatherService {
fun getWeather(city: String): Single<WeatherData>
}
Implementation
city = BehaviorSubject.createDefault("")
state = BehaviorSubject.createDefault(Idle)
weather = city
.filter { it.isNotEmpty() }
.debounce(500, TimeUnit.MILLISECONDS, mainScheduler)
.switchMap {
weatherService.getWeather(it)
.observeOn(mainScheduler)
.doOnSubscribe { state.onNext(Loading) }
.doOnSuccess { state.onNext(Idle) }
.doOnError { state.onNext(Error(it)) }
.toObservable()
.onErrorResumeNext(Observable.empty())
}
Use case diagram
For example, in this diagram, the user starts typing “Toulouse”, and after 500ms without activity (no keystroke pressed), we call the web service to get the weather in Toulouse. The web service then returns the response (sunny weather). Afterwards, the user wants to check the weather in Paris, so after the delay, the web service is called, and then we get the response.
Marble testing implementation
@Before
fun setup() {
weatherService = Mockito.mock(WeatherService::class.java)
scheduler = MarbleScheduler(100)
viewModel = WeatherViewModelImpl(weatherService, scheduler)
}
Following are the values that we need in order to test. We map the symbol “0” to the event “empty string”, the symbol “1” to the event the user inputs “tou”, the symbol “t” to the event the user inputs “toulouse”, etc.
val cityValues = mapOf(
"0" to "",
"1" to "tou",
"t" to "toulouse",
"b" to "bordeaux"
)
val stateValues = mapOf(
"i" to Idle,
"l" to Loading,
"e" to Error(weatherError)
)
val weatherValues = mapOf(
"t" to weatherData,
"b" to bordeauxData
)
And these are the data that the webservice is mocked to respond.
val weatherData = WeatherData("toulouse", "sunny", 20f, 30f)
val bordeauxData = WeatherData("bordeaux", "cloudy", 10f, 15f)
So now, the test looks like this.
@Test fun test2Cities() {
val s = scheduler
val cityInput = s.hot( "0-1-t------------b----------", cityValues)
// debouncing -----t -----b
`when`(weatherService.getWeather("toulouse"))
.thenReturn(s.single( "--t", weatherValues))
`when`(weatherService.getWeather("bordeaux"))
.thenReturn(s.single( "--b", weatherValues))
s.expectObservable(viewModel.weather).toBe( "-----------t------------b---", weatherValues)
s.expectObservable(viewModel.state).toBe( "i--------l-i----------l-i---", stateValues)
cityInput.subscribe(viewModel.city)
s.flush()
}
We obtain an ASCII visual representation of what we simulate the user interaction to be, and then, we tell the test what chain of events we expect to receive from the various observables. In this representation, we can visually check how the different timelines correspond, and easily test that the more complex chains of events actually lead to the observable that we want.
Conclusion
Pros
- Tests are more concise and expressive
- Complex cases can be tested visually
- Now testing the global coherence and behaviour is made possible.
Cons
- The API suffers from differences between the different platform.
- Alignment of marbles can be visually challenging in ASCII.
The speaker concluded by proposing improvements in the future in order to counter the cons:
- Uniformisation of the APIs.
- Development of a graphical editor for marbles.
He added that if someone in the conference wanted to get involved and develop a graphical editor, it would be great and useful.