Reputation: 5935
void start() {
bar.foo()
.filter(i -> i % 2 == 0)
.subscribeOn(computation())
.observeOn(io())
.subscribe(new FooSubscriber());
}
In this function I see 3 points to test:
bar.foo()
.filter
is correctly implemented.bar.foo()
.The first point is easy to test with Mockito.verify()
. The third point I can inject the Schedulers and use Schedulers.immediate()
and then mock the observer with a Subject and check Subject.hasObservers()
. But I have no idea about how to test the second point.
How can I test this code? Must I refactor it? How?
Please, think that filter
is just an example, I have a big chain with different operators.
Upvotes: 1
Views: 1027
Reputation: 720
Difficult to test this method as there is no "observable" behavior to assert on and you need to get your test code "in the way" of the logic.
Here is a simple approach you could follow: (although you might want to consider breaking things up to make testing easier)
Mock foo, verify that bar() is called if you need to, return a real Observable from bar() that will unwind the callback chain when subscribe is called. - this will test that your chain is wired up as expected.
Inject schedulers that execute the logic on the main thread in a blocking manner thus keeping the tests synchronous and easy to understand
Extract new FooSubscriber()
to a package private method and use Mockito to spy the new method, returning a test subscriber that makes assertions on the filtered data emitted from the observable - or - inject a factory class that builds instances of FooSubscriber that you can mock for testing purposes by returning a test subscriber. - basically the hardcoded use of the new keyword is blocking you from testing the behavior.
I can provide example if you need, hope this gets you going.
EDIT: example of both methods described above:
package com.rx;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import rx.Observable;
import rx.Observer;
import rx.Scheduler;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
@RunWith(MockitoJUnitRunner.class)
public class TestRxMethod {
// prod Bar class - this class tested in isolation in different test.
public static class Bar {
public Observable<Integer> foo() {
return null;
}
}
// prod FooSubscriber class - this class tested in isolation in different test.
public static class FooSubscriber implements Observer<Integer> {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Integer t) {
}
}
// prod FooSubscriberFactory class - this class tested in isolation in different test.
public static class FooSubscriberFactory {
public Observer<Integer> getInstance() {
return null;
}
}
// prod "class under test"
public static class UnderTest {
private final Bar bar;
private final Scheduler computationScheduler;
private final Scheduler ioScheduler;
private final FooSubscriberFactory fooSubscriberFactory;
public UnderTest(Bar bar, Scheduler computationScheduler, Scheduler ioScheduler,
FooSubscriberFactory fooSubscriberFactory) {
this.bar = bar;
this.computationScheduler = computationScheduler;
this.ioScheduler = ioScheduler;
this.fooSubscriberFactory = fooSubscriberFactory;
}
public void start() {
//@formatter:off
bar.foo()
.filter(i -> i.intValue() % 2 == 0)
.subscribeOn(computationScheduler)
.observeOn(ioScheduler)
.subscribe(fooSubscriber());
//@formatter:on
}
// package private so can be overridden by unit test some drawbacks
// using this strategy like class cant be made final. - use only
// if cant restructure code.
Observer<Integer> fooSubscriber() {
return fooSubscriberFactory.getInstance();
}
}
// test Foo subscriber class - test will put set an instance of
// this class as the observer on the callback chain.
public static class TestFooSubscriber implements Observer<Integer> {
public List<Integer> filteredIntegers = new ArrayList<>();
@Override
public void onCompleted() {
// noop
}
@Override
public void onError(Throwable e) {
// noop
}
@Override
public void onNext(Integer i) {
// aggregate filtered integers for later assertions
filteredIntegers.add(i);
}
}
// mock bar for test
private Bar bar;
// mock foo subscriber factory for test
private FooSubscriberFactory fooSubscriberFactory;
// class under test - injected with test dependencies
private UnderTest underTest;
@Before
public void setup() {
bar = Mockito.mock(Bar.class);
fooSubscriberFactory = Mockito.mock(FooSubscriberFactory.class);
underTest = new UnderTest(bar, Schedulers.immediate(), Schedulers.immediate(), fooSubscriberFactory);
}
// Option #1 - injecting a factory
@Test
public void start_shouldWork_usingMockedFactory() {
// setup bar mock to emit integers
Mockito.when(bar.foo()).thenReturn(Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
// setup the subscriber factory to produce an instance of the test subscriber
TestFooSubscriber testSubscriber = new TestFooSubscriber();
Mockito.when(fooSubscriberFactory.getInstance()).thenReturn(testSubscriber);
underTest.start();
Assert.assertEquals(5, testSubscriber.filteredIntegers.size());
// ... add more assertions as needed per the use cases ...
}
// Option #2 - spying a protected method
@Test
public void start_shouldWork_usingSpyMethod() {
// setup bar mock to emit integers
Mockito.when(bar.foo()).thenReturn(Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
// spy the class under test (use only as a last resort option)
underTest = Mockito.spy(underTest);
TestFooSubscriber testSubscriber = new TestFooSubscriber();
Mockito.when(underTest.fooSubscriber()).thenReturn(testSubscriber);
underTest.start();
Assert.assertEquals(5, testSubscriber.filteredIntegers.size());
// ... add more assertions as needed per the use cases ...
}
}
Upvotes: 1