1. Обзор
В этой статье мы рассмотрим способы тестирования кода, написанного с использованием RxJava .
Типичный поток, который мы создаем с помощью RxJava, состоит из Observable
и Observer.
Наблюдаемое — это источник данных, который представляет собой последовательность элементов. Один или несколько наблюдателей подписываются на него для получения генерируемых событий.
Как правило, наблюдатель и наблюдаемые объекты выполняются в отдельных потоках асинхронным образом, что затрудняет тестирование кода традиционным способом.
К счастью, RxJava предоставляет класс TestSubscriber
, который дает нам возможность тестировать асинхронный поток, управляемый событиями.
2. Тестирование RxJava — традиционный способ
Начнем с примера — у нас есть последовательность букв, которую мы хотим заархивировать последовательностью целых чисел от 1 включительно.
Наш тест должен утверждать, что подписчик, который прослушивает события, испускаемые заархивированным наблюдаемым, получает письма, заархивированные с целыми числами.
Написание такого теста традиционным способом означает, что нам нужно вести список результатов и обновлять этот список от наблюдателя. Добавление элементов в список целых чисел означает, что наши наблюдаемые объекты и наблюдатели должны работать в одном потоке — они не могут работать асинхронно.
Таким образом, мы бы упустили одно из самых больших преимуществ RxJava — обработку событий в отдельных потоках.
Вот как будет выглядеть эта ограниченная версия теста:
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
List<String> results = new ArrayList<>();
Observable<String> observable = Observable
.from(letters)
.zipWith(
Observable.range(1, Integer.MAX_VALUE),
(string, index) -> index + "-" + string);
observable.subscribe(results::add);
assertThat(results, notNullValue());
assertThat(results, hasSize(5));
assertThat(results, hasItems("1-A", "2-B", "3-C", "4-D", "5-E"));
Мы собираем результаты от наблюдателя, добавляя элементы в список результатов
. Наблюдатель и наблюдаемое работают в одном и том же потоке, поэтому наше утверждение правильно блокируется и ожидает завершения метода subscribe() .
3. Тестирование RxJava с помощью TestSubscriber
RxJava поставляется с классом TestSubscriber
, который позволяет нам писать тесты, работающие с асинхронной обработкой событий. Это обычный наблюдатель, который подписывается на наблюдаемое.
В тесте мы можем проверить состояние TestSubscriber
и сделать утверждения об этом состоянии:
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
TestSubscriber<String> subscriber = new TestSubscriber<>();
Observable<String> observable = Observable
.from(letters)
.zipWith(
Observable.range(1, Integer.MAX_VALUE),
((string, index) -> index + "-" + string));
observable.subscribe(subscriber);
subscriber.assertCompleted();
subscriber.assertNoErrors();
subscriber.assertValueCount(5);
assertThat(
subscriber.getOnNextEvents(),
hasItems("1-A", "2-B", "3-C", "4-D", "5-E"));
Мы передаем экземпляр TestSubscriber методу
subscribe()
наблюдаемого объекта. Затем мы можем проверить состояние этого подписчика.
TestSubscriber
имеет несколько очень полезных методов утверждения, которые мы будем использовать для проверки наших ожиданий. Подписчик должен получить 5 испущенных элементов наблюдателем, и мы утверждаем это, вызывая метод assertValueCount()
.
Мы можем проверить все события, которые получил подписчик, вызвав метод getOnNextEvents()
.
Вызов метода assertCompleted()
проверяет, завершен ли поток, на который подписан наблюдатель. Метод assertNoErrors()
утверждает, что при подписке на поток ошибок не было.
4. Тестирование ожидаемых исключений
Иногда в нашей обработке, когда наблюдаемое испускает события или наблюдатель обрабатывает события, возникает ошибка. У TestSubscriber
есть специальный метод для проверки состояния ошибки — метод assertError()
, который принимает в качестве аргумента тип исключения:
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
TestSubscriber<String> subscriber = new TestSubscriber<>();
Observable<String> observable = Observable
.from(letters)
.zipWith(Observable.range(1, Integer.MAX_VALUE), ((string, index) -> index + "-" + string))
.concatWith(Observable.error(new RuntimeException("error in Observable")));
observable.subscribe(subscriber);
subscriber.assertError(RuntimeException.class);
subscriber.assertNotCompleted();
Мы создаем наблюдаемую, которая соединяется с другой наблюдаемой с помощью метода concatWith()
. Второй наблюдаемый генерирует исключение RuntimeException
при генерации следующего события. Мы можем проверить тип этого исключения в TestSubsciber
, вызвав метод assertError()
.
Наблюдатель, получивший ошибку, прекращает обработку и оказывается в незавершенном состоянии. Это состояние можно проверить методом assertNotCompleted()
.
5. Тестирование наблюдаемых на основе времени
Допустим, у нас есть Observable
, который генерирует одно событие в секунду, и мы хотим протестировать это поведение с помощью TestSubsciber
.
Мы можем определить Observable на основе времени с
помощью метода Observable.interval()
и передать TimeUnit
в качестве аргумента:
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
TestScheduler scheduler = new TestScheduler();
TestSubscriber<String> subscriber = new TestSubscriber<>();
Observable<Long> tick = Observable.interval(1, TimeUnit.SECONDS, scheduler);
Observable<String> observable = Observable.from(letters)
.zipWith(tick, (string, index) -> index + "-" + string);
observable.subscribeOn(scheduler)
.subscribe(subscriber);
Наблюдаемый тик
будет испускать новое значение каждую секунду.
В начале теста мы находимся в нулевом времени, поэтому наш TestSubscriber
не будет завершен:
subscriber.assertNoValues();
subscriber.assertNotCompleted();
Чтобы эмулировать течение времени в нашем тесте, нам нужно использовать класс TestScheduler
. Мы можем имитировать этот односекундный проход, вызвав метод advanceTimeBy()
в TestScheduler
:
scheduler.advanceTimeBy(1, TimeUnit.SECONDS);
Метод advanceTimeBy()
заставит наблюдаемое произвести одно событие. Мы можем утверждать, что было создано одно событие, вызвав метод assertValueCount()
:
subscriber.assertNoErrors();
subscriber.assertValueCount(1);
subscriber.assertValues("0-A");
Наш список букв
содержит 5 элементов, поэтому, когда мы хотим, чтобы наблюдаемый объект генерировал все события, необходимо пройти 6 секунд обработки. Чтобы эмулировать эти 6 секунд, мы используем метод advanceTimeTo()
:
scheduler.advanceTimeTo(6, TimeUnit.SECONDS);
subscriber.assertCompleted();
subscriber.assertNoErrors();
subscriber.assertValueCount(5);
assertThat(subscriber.getOnNextEvents(), hasItems("0-A", "1-B", "2-C", "3-D", "4-E"));
После эмуляции прошедшего времени мы можем выполнять утверждения для TestSubscriber
. Мы можем утверждать, что все события были произведены, вызвав метод assertValueCount()
.
6. Заключение
В этой статье мы рассмотрели способы тестирования наблюдателей и наблюдаемых в RxJava. Мы рассмотрели способ тестирования генерируемых событий, ошибок и наблюдаемых во времени.
Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.