MarbleTest4j
Java port of RxJS marble tests, works with RxJava, RxJava2 and Reactor3
MarbleTest4j is a tiny library that allows to write tests using marble diagrams in ASCII form.
This is a Java port of the marble test features of amazing RxJS v5.
The purpose of the library is to help you write as concise an readable tests when dealing with reactive code, bringing a developer experience as close as possible as the one of RxJS.
Check out this nice 7 minutes introduction on egghead.io to get up to speed with RxJS marble testing.
Quickstart
To get the lib just use add a maven dependency as below:
<dependency>
<groupId>com.github.alexvictoor</groupId>
<artifactId>marbletest4j</artifactId>
<version>1.3</version>
</dependency>
You will need also to import the reactive library used in your project (RxJava, RxJava2 or Reactor3).
When using Reactor3, you also need to import the reactor-test module as follow:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-test</artifactId>
<version>${reactor.version}</version>
</dependency>
Usage (the concise way)
A jUnit integration is provided in order to let you write concise tests as you would have done with RxJS. This integration is made of a jUnit rule MarbleRule and a bunch of static methods providing aliases to MarbleScheduler's methods. MarbleScheduler is very similar to RxJS TestScheduler: it is like RxJava's TestScheduler plus marble related methods to create hot & cold test observables and then perform assertions. Obviously everything is done using marble schemas in ASCII form.
MarbleRule keeps in a threadlocal reference a MarbleScheduler instance that will be used by static aliases methods. Though, for most cases you will not need to manipulate directly any scheduler.
marbletest4j is compatible with RxJava and RxJava2. Depending on the flavor of RxJava in use in your project, you need to use a different package prefix when importing marbletest4j classes:
- rx.marble & rx.marble.junit for RxJava1 projects
- io.reactivex.marble & io.reactivex.marble.junit for RxJava2 projects
- reactor & reactor.junit for Reactor3 projects
Below a complete RxJava2 example:
import static io.reactivex.marble.junit.MarbleRule.*;
// import static rx.marble.junit.MarbleRule.*; if RxJava1 is used
@Rule
public MarbleRule marble = new MarbleRule();
@Test
public void should_map() {
// given
Observable<String> input = hot("a-b--c---d");
// when
Observable<String> output = input.map(s -> s.toUpperCase());
// then
expectObservable(output).toBe( "A-B--C---D");
}
In the example above, we create first a hot observable trigering events 'a', 'b', 'c', 'd' (at 0, 20, 50 and 90)
Then we perform some transformations, using rx map operator, and last we perform an assertion on generated Observable.
If you are into Reactor3, the API is quite similar as you can see in the simple example below:
import static reactor.MapHelper.of;
import static reactor.junit.MarbleRule.*;
@Rule
public MarbleRule marbleRule = new MarbleRule();
@Test
public void should_concat_with_scan() {
// given
HotFlux<String> flux = hot( "--a--b--c");
// when
Flux<String> concatFlux = flux.scan((x, y) -> x + " " + y);
// then
expectFlux(concatFlux).toBe("--A--B--C", of("A", "a", "B", "a b", "C", "a b c"));
}
In previous examples, event values were strings, other types are also supported. Below an RxJava2 example but obviously reactor API is once again quite similar:
import static io.reactivex.marble.junit.MarbleRule.*;
import static io.reactivex.marble.MapHelper.of;
// for RxJava1 replace by the following imports
// import static rx.marble.junit.MarbleRule.*;
// import static rx.marble.MapHelper.of;
@Rule
public MarbleRule marble = new MarbleRule();
@Test
public void should_subscribe_during_the_test() {
Map<String, Integer> values = of("a", 1, "b", 2); // shortcut from class MapHelper to create a Map
ColdObservable<Integer> myObservable
= cold( "---a---b--|", values);
String subscription = "^---------!";
expectObservable(myObservable).toBe("---a---b--|", values);
expectSubscriptions(myObservable.getSubscriptions()).toBe(subscription);
}
As shown above, you can check events timing and values, but also when subscriptions start and end.
Everything in a visual way using marble diagrams in ASCII forms :-)
Usage (the verbose way)
As said before, the API sticks to the RxJS one. The cornerstone of this API is the MarbleScheduler class. Below an example showing how to initiate a scheduler:
MarbleScheduler scheduler = new MarbleScheduler();
This scheduler can then be used to configure source observables:
Observable<String> sourceEvents = scheduler.createColdObservable("a-b-c-|");
Then you can use the MarbleScheduler.expectObservable() to verify that everything went as expected during the test. Below a really simple all-in-one example:
MarbleScheduler scheduler = new MarbleScheduler();
Observable<String> sourceEvents = scheduler.createColdObservable("a-b-c-|"); // create an IObservable<string> emiting 3 "next" events
Observable<String> upperEvents = sourceEvents.select(s -> s.toUpperCase()); // transform the events - this is what we often call the SUT ;)
scheduler.expectObservable(upperEvents).toBe("A-B-C-|"); // check that the output events have the timing and values as expected
scheduler.flush(); // let the virtual clock goes... otherwise nothing happens
Important: as shown above, do not forget to flush the scheduler at the end of your test case, otherwise no event will be emitted.
In the above examples, event values are not specified and string streams are produced (i.e. Observable).
As with the RxJS api, you can use a parameter map/hash containing event values:
Map<String, Integer> values = new HashMap<>();
values.put("a", 1);
values.put("b", 2);
values.put("c", 3);
Observable<Integer> events = scheduler.CreateHotObservable<int>("a-b-c-|", values);
In order to reduce the amount of boilerplate code needed to create an iniate the map, you can use guava's ImmutableMap or MapHelper static methods:
import static rx.marble.MapHelper.of;
...
Observable<Integer> events
= scheduler.CreateHotObservable<int>("a-b-c-|", of("a", 1, "b", 2, "c", 3));
Marble ASCII syntax
The syntax remains exactly the same as the one of RxJS.
Each ASCII character represents what happens during a time interval, by default 10 ticks.
'-' means that nothing happens
'a' or any letter means that an event occurs
'|' means the stream end successfully
'#' means an error occurs
So "a-b-|" means:
- At 0, an event 'a' occurs
- Nothing till 20 where an event 'b' occurs
- Then the stream ends at 40
If some events occurs simultanously, you can group them using paranthesis.
So "--(abc)--" means events a, b and c occur at time 20.
For an exhaustive description of the syntax you can checkout the official RxJS documentation
Advanced features
For a complete listof supported features you can checkout the tests of the MarbleScheduler class.