Тестирование и отладка наблюдаемых последовательностей
Тестирование приложения Rx
Если у вас есть наблюдаемая последовательность, которая публикует значения в течение длительного периода времени, ее тестирование в режиме реального времени может быть растянутым. Библиотека реактивных расширений предоставляет тип TestScheduler , который помогает тестировать код, зависящий от времени, фактически не дожидаясь истечения времени. TestScheduler наследует VirtualScheduler и позволяет создавать, публиковать последовательности и подписываться на них в эмулированное время. Например, можно сжать публикацию, выполнение которой занимает 5 дней, в 2 минуты, сохраняя при этом правильный масштаб. Вы также можете взять последовательность, которая фактически произошла в прошлом (например, последовательность акций за предыдущий год) и вычислить или подписаться на нее, как если бы она выталкивала новые значения в режиме реального времени.
Заводской метод Start выполняет все запланированные задачи до тех пор, пока очередь не пуста, или вы можете указать время, чтобы задачи, ранее помещенные в очередь, выполнялись только в указанное время.
В следующем примере создается горячая наблюдаемая последовательность с указанными уведомлениями OnNext. Затем он запускает планировщик тестов и указывает, когда следует подписаться на горячую наблюдаемую последовательность и удалить ее. Метод Start возвращает экземпляр ITestableObserver, который содержит свойство Messages, которое записывает все уведомления в список.
После завершения последовательности мы используем метод ReactiveAssert.AreElementEqual для сравнения свойства Messages вместе со списком ожидаемых значений, чтобы увидеть, совпадают ли оба значения (с одинаковым количеством элементов, а элементы равны и в одном порядке). Таким образом, мы можем подтвердить, что мы действительно получили ожидаемые уведомления. В нашем примере, так как подписка начинается только с 150, мы будем пропускать значение abc
. Однако при сравнении значений, полученных до сих пор на уровне 400, мы замечаем, что на самом деле мы получили все опубликованные значения после подписки на последовательность. Мы также проверяем, что OnCompleted
уведомление было сработало в нужное время в 500. Кроме того, сведения о подписке также записываются типом ITestableObservable, возвращаемым методом CreateHotObservable.
Аналогичным образом можно использовать ReactiveAssert.AreElementsEqual, чтобы убедиться, что подписки действительно произошли в ожидаемое время.
using System;
using System.Reactive;
using System.Reactive.Linq;
using Microsoft.Reactive.Testing;
class Program : ReactiveTest
{
static void Main(string[] args)
{
var scheduler = new TestScheduler();
var input = scheduler.CreateHotObservable(
OnNext(100, "abc"),
OnNext(200, "def"),
OnNext(250, "ghi"),
OnNext(300, "pqr"),
OnNext(450, "xyz"),
OnCompleted<string>(500)
);
var results = scheduler.Start(
() => input.Buffer(() => input.Throttle(TimeSpan.FromTicks(100), scheduler))
.Select(b => string.Join(",", b)),
created: 50,
subscribed: 150,
disposed: 600);
ReactiveAssert.AreElementsEqual(results.Messages, new Recorded<Notification<string>>[] {
OnNext(400, "def,ghi,pqr"),
OnNext(500, "xyz"),
OnCompleted<string>(500)
});
ReactiveAssert.AreElementsEqual(input.Subscriptions, new Subscription[] {
Subscribe(150, 500),
Subscribe(150, 400),
Subscribe(400, 500)
});
}
}
Отладка приложения Rx
Для отладки приложения Rx можно использовать оператор Do. Оператор Do позволяет указать различные действия, выполняемые для каждого элемента наблюдаемой последовательности (например, печать или регистрация элемента и т. д.). Это особенно полезно, если вы цепочки множества операторов и хотите знать, какие значения создаются на каждом уровне.
В следующем примере мы будем повторно использовать пример буфера, который создает целые числа каждую секунду, помещая их в буферы, в которых может содержаться по 5 элементов. В нашем исходном примере в разделе Запрос наблюдаемых последовательностей с помощью операторов LINQ мы подписываемся только на окончательную последовательность Observable(IList<>), когда буфер заполнен (и до его очистки). Однако в этом примере мы будем использовать оператор Do для вывода значений при их отправке исходной последовательностью (целое число каждую секунду). Когда буфер заполнен, мы используем оператор Do для вывода состояния, прежде чем передавать все это в качестве окончательной последовательности для подписки наблюдателя.
var seq1 = Observable.Interval(TimeSpan.FromSeconds(1))
.Do(x => Console.WriteLine(x.ToString()))
.Buffer(5)
.Do(x => Console.WriteLine("buffer is full"))
.Subscribe(x => Console.WriteLine("Sum of the buffer is " + x.Sum()));
Console.ReadKey();
Как видно из этого примера, подписка находится в конце получателя ряда цепочек наблюдаемых последовательностей. Сначала мы создаем наблюдаемую последовательность целых чисел, разделенных на секунду с помощью оператора Interval. Затем мы помещаем 5 элементов в буфер с помощью оператора Buffer и отправляем их как другую последовательность только при заполнении буфера. Наконец, он передается оператору Subscribe. Данные распространяются на все эти промежуточные последовательности, пока они не будут отправлены наблюдателю. Аналогичным образом подписки распространяются в обратном направлении на исходную последовательность. Вставив оператор Do в середине такого распространения, вы можете "шпионить" за таким потоком данных так же, как вы используете Console.WriteLine в .NET или printf() в C для выполнения отладки.
Оператор Timestamp также можно использовать для проверки времени, когда элемент отправляется наблюдаемой последовательностью. Это поможет устранить неполадки с операциями на основе времени, чтобы обеспечить точность. Помните следующий пример из раздела Создание и подписка на простые отслеживаемые последовательности , в котором мы связываем оператор Timestamp с запросом, чтобы каждое значение, вытесненное исходной последовательностью, добавлялось к моменту публикации. Таким образом, когда мы подписываемся на эту исходную последовательность, мы можем получить как ее значение, так и метку времени.
Console.WriteLine(“Current Time: “ + DateTime.Now);
var source = Observable.Timer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(1))
.Timestamp();
using (source.Subscribe(x => Console.WriteLine("{0}: {1}", x.Value, x.Timestamp)))
{
Console.WriteLine("Press any key to unsubscribe");
Console.ReadKey();
}
Console.WriteLine("Press any key to exit");
Console.ReadKey();
Результат должен быть аналогичен следующему:
Current Time: 5/31/2011 5:35:08 PM
Press any key to unsubscribe
0: 5/31/2011 5:35:13 PM -07:00
1: 5/31/2011 5:35:14 PM -07:00
2: 5/31/2011 5:35:15 PM -07:00
С помощью оператора Timestamp мы убедились, что первый элемент действительно отправляется через 5 секунд после последовательности, а каждый элемент публикуется через 1 секунду.
Кроме того, можно также задать точки останова внутри лямбда-выражений, чтобы упростить отладку. Как правило, точку останова можно задать только для всего запроса, не высвечивая определенное значение. Чтобы обойти это ограничение, можно вставить оператор Select в середину запроса и задать для него точку останова, а в операторе Select проецировать идентичное значение в качестве источника с помощью оператора return в собственной строке. Затем можно задать точку останова в строке инструкции return
и изучить значения по мере их прохождения в запросе.
var seq = Observable.Interval(TimeSpan.FromSeconds(1))
.Do(x => Console.WriteLine(x.ToString()))
.Buffer(5)
.Select(y => {
return y; }) // set a breakpoint at this line
.Do(x => Console.WriteLine("buffer is full"))
.Subscribe(x => Console.WriteLine("Sum of the buffer is " + x.Sum()));
Console.ReadKey();
В этом примере точка останова устанавливается в строке return y
. При отладке в программе y
переменная отображается в окне Локальные , и вы можете проверить ее общее количество (5)
. Если развернуть y
, можно также проверить каждый элемент в списке, включая его значение и тип.
Кроме того, можно преобразовать лямбда-выражение в лямбда-выражение оператора, отформатировать код так, чтобы оператор был в отдельной строке, а затем задать точку останова.
После завершения отладки можно удалить все вызовы Do и Select.
См. также:
Основные понятия
Создание и подписка на простые отслеживаемые последовательности
Запрос наблюдаемых последовательностей с помощью операторов LINQ
Использование планировщиков