Trust is good control is better – JUnit Tests (parametrized and different arguments providers) with RxJava and some Mockito
A follow up to Software tests – some basic JUnit testing in the area of RxJava. Not much text right now, just the announcement, the code below and the link to my github repo. Includes a build.gradle with all the needed dependencies.
(There are already some other test concerning ConnectableObservables subscriptions and time of emit.)
Short summary:
How to test classes using RxJava Observables with JUnit. Description in the Java file as comment.
- TestObserver assertions
- mocking with Mockito
- mocks returning Observables from Observable.just(…) and BehaviorSubject
- parametrized tests with value sources annotation, method and AgrumentProvider
package rxjava;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.stream.Stream;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class ObservableMockingTest {
@Mock ValueProvider valueProvider;
ValueProcessor valueProcessor;
@BeforeEach
void setUp(){
valueProcessor = new ValueProcessor(valueProvider);
}
@Test
void value8_emits64(){
// when testing only one or a fixed list of emitted values is enough, mocking with Observable.just(...values)
// is sufficient
when(valueProvider.values()).thenReturn(Observable.just(8));
valueProcessor.processedValue().test().assertValue(i -> i == 64);
}
@Test
void values_multiple_emitsProcessed(){
// when testing multiple values, transitions or timing of the emit - I like to use BehaviorSubjects
BehaviorSubject<Integer> values = BehaviorSubject.create();
when(valueProvider.values()).thenReturn(values);
values.onNext(8);
valueProcessor.processedValue().test().assertValue(i -> i == 64);
values.onNext(10);
valueProcessor.processedValue().test().assertValue(i -> i == 100);
}
// =============================== parametrized tests
// Very useful if you want to test a class with a set of values
// I will list some possibilities how to achieve your test goal
@ParameterizedTest
@ValueSource(ints = {8, 10, 25})
void values_valueSource_emitsProcessed(int value){
// simple, but very limited
when(valueProvider.values()).thenReturn(Observable.just(value));
valueProcessor.processedValue().test().assertValue(i -> i == value*value);
}
static Stream<Arguments> yourTestProviderMethodName(){
// Arguments allow every and multiple types - but it has to match the test method signature
// return Stream.of(arguments(true, "NOPE", 8, NullPointerException.class, MyEnum.VALUE_OFF), ...);
return Stream.of(arguments(8), arguments(10), arguments(25));
}
@ParameterizedTest
@MethodSource(value = "yourTestProviderMethodName")
void values_test_methodSource(int value){
// When using MethodSource you have to provide the name of the method providing the test parameters
// This is error prone because misspelling or renaming the method breaks this test.
// Can get a little messy when you have multiple similar named provider methods
// For this case definitely too much - just want to show
when(valueProvider.values()).thenReturn(Observable.just(value));
valueProcessor.processedValue().test().assertValue(i -> i == value*value);
}
static class YourTestArgumentProviderClass implements ArgumentsProvider{
// Instead of just a provider method you can implement your ArgumentsProvider class. provideArguments is called
// and you logic returning the desired test values has to be in there
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(arguments(8), arguments(10), arguments(25));
}
}
@ParameterizedTest
@ArgumentsSource(YourTestArgumentProviderClass.class)
void values_test_argumentProvider(int value){
// Using an ArgumentsProvider removes relying on the matching method name String and shows clearly what the
// provider is instead of just another method whose name you have to copy and search in the test. Using produces
// some (automatically generated) boilerplate code, but makes it much clearer as it is defined as an
// ArgumentsProvider and the structure is uniform. Plus you can STRG+click -> jump there from the test
//
// I learned this when using @Nested class in a test and was not able to define a MethodSource in a nested
// test class, but was able to use an ArgumentsProvider instead
when(valueProvider.values()).thenReturn(Observable.just(value));
valueProcessor.processedValue().test().assertValue(i -> i == value*value);
}
}