Rx принес некоторые интересные проблемы в сообщество TDD, ибо производить тестирование асинхронной модели задача не из простых, и требует иной подход нежели тестирование синхронного кода. В данной статье будут рассмотрены способы тестирования Rx модели.
На заметку. После знака @ (at) показано время, когда было вытолкнуто значение (((9000000000 tic/10000)msec)/1000)sec/60)min = 15 мин.
Результат:
Во-втором примере все точно также:
Результат:
Для более удобного использования HotObservable/ColdObservable существыют ExtensionsMethods для TestScheduler:
сюрпризов интересных методов, например, проверка на равенство двух Observable. Но это уже для самостоятельного исследования.... и рефлектор в помощь ;)
Помните:
"Documentation is like sex. If it is good, it is REALLY good, and if it is bad, it is better than nothing!" Gabe Helou
Запись событий и анализ
Класс MockObserver<> записывает данные которые "выталкивает" Observable в плоскую коллекцию. Данная коллекция содержит элементы в той последовательности, с которой они были записаны. Кроме того, элементы содержат временной штамп, который показывает в какой момент времени он был помещен в коллекцию. У каждого события есть "вид". Их всего три Next, Complete, Error. Все эти свойства дают крепкий фундамент для анализа результата. Рассмотрим несколько примеров:[Test]
public void Subscribe_should_push_3_values_and_complete()
{
var observable = Observable.Create<int>(x =>
{
x.OnNext(1);
x.OnNext(2);
x.OnNext(3);
x.OnCompleted();
return () => { };
});
var testScheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(testScheduler);
observable.Subscribe(mockObserver);
Assert.That(mockObserver[0].Value.Value, Is.EqualTo(1));
Assert.That(mockObserver[0].Value.Kind, Is.EqualTo(NotificationKind.OnNext));
Assert.That(mockObserver[1].Value.Value, Is.EqualTo(2));
Assert.That(mockObserver[1].Value.Kind, Is.EqualTo(NotificationKind.OnNext));
Assert.That(mockObserver[2].Value.Value, Is.EqualTo(3));
Assert.That(mockObserver[2].Value.Kind, Is.EqualTo(NotificationKind.OnNext));
Assert.That(mockObserver[3].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
Hа 4 строке создается последовательность из 3-х целых чисел после чего последовательность завершается. На 14 строке создается экземпляр MockObserver<>, которому в качестве аргумента конструктора передан экземпляр класса TestScheduler. Данный класс является реализацией интерфейса IScheduler и необходим для эмуляции планировщика. Далее он будет рассмотрен отдельным пунктом. На строке 16 происходит подписка и запись результата. Далее идет анализ результата. Класс MockObserver<> позволяет с помощью индекса получить доступ к нужной "записи" (Recorded<>) события и проанализировать его. Данный класс предоставляет два важныx свойства:
- Time - смещение от момента подписки до возникновение события в тиках
- Value - источник события
- Kind - перечисление рода события OnNext/OnError/OnCompleted
- Value - значение, переданное в качестве аргумента события.
[Test]
public void Subscribe_should_push_value_with_delay_and_complete()
{
var testScheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(testScheduler);
var observable = Observable.Return(42);//Answers to basic questions of life, the universe and everything else :)
observable
.Delay(TimeSpan.FromMilliseconds(1), testScheduler)
.Subscribe(mockObserver);
testScheduler.Run();
Assert.That(mockObserver[0].Value.Value, Is.EqualTo(42));
Assert.That(mockObserver[0].Time, Is.EqualTo(10000));//tiks per milliseconds
Assert.That(mockObserver[0].Value.Kind, Is.EqualTo(NotificationKind.OnNext));
Assert.That(mockObserver[1].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
После инициализации observable на 7 строке, перед подпиской выполняется пауза в одну миллисекунду, прежде чем "вытолкнется" значение. Затем запускается планировщик (см. ниже зачем это нужно) и производится тестирование результатов. Тут все как и в прошлом примере, за исключением свойства "Time". В данном свойстве и содержится значение задержки после подписки и перед получением значения. Напоминаю, что значение свойства "Time" в тиках.
Планировщик TestScheduler
Планировщик является неотъемлемой частью Rx, непонимание его принципов ведет к трудноуловимым ошибкам и Deadlock'ам. Планировщик выполняет работу кода определенным способом. Всего существует 7 реализаций планировщика, это:- Scheduler.Dispatcher выполняет работу в текущем "Dispatcher", без которого не обойтись в приложениях Silverlight и WPF. Другими словами, реализация этого планировщика - это просто делегирование кода в "Dispatcher" (Dispatcher.BeginInvoke(Action)).
- Scheduler.NewThread выполняет работу в новом потоке.
- Scheduler.ThreadPool выполняет работу в пуле потоков.
- Scheduler.TaskPool выполняет работу в пуле задач.
- Scheduler.Immediate немедленно выполняет работу в текущем потоке.
- Scheduler.CurrentThread выполняет работу в текущем потоке. Разница между Immediate в том, что CurrentThread ставит работу в очередь выполнения.
- TestScheduler эмулирует работу планировщика.
- Выполнение работы подписки (SubscribeOn())
- Публикация результатов работы (ObserveOn())
var scheduler = new TestScheduler();
var oneMinentsTiks = scheduler.FromTimeSpan(TimeSpan.FromMinutes(1));
var twoMinentsTiks = scheduler.FromTimeSpan(TimeSpan.FromMinutes(2));
scheduler.Schedule(() => Debug.WriteLine("1"), oneMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("2"), oneMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("3"), twoMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("4"), twoMinentsTiks);
Debug.WriteLine("RunTo(oneMinentsTiks)");
scheduler.RunTo(oneMinentsTiks);
Debug.WriteLine("Run()");
scheduler.Run();
После того, как планировщик создан, идет “планирование” выполнения двух операций на 4-м тике, затем еще две операции на 5 тике. Таким образом последняя операция выполнится, когда на часах будет 5 тиков. Затем на строке 10 планировщик выполняет операции, которые должны быть выполнены до 4-го тика, а затем и все запланированные. Результат выполнения этого кода представлен ниже:
/* Output: RunTo(oneMinentsTiks) 1 2 Run() 3 4 */Далее рассмотрим более жизненный пример. Необходимо запрашивать данные с сервера через каждых 15 мин. на протяжении 2-х часов. Понятно, что при тестировании не хочется получить результат теста через два часа, а сразу.
[TestFixture]
public class TestSchedulerTest
{
[Test]
public void Subscrible_should_push_values_for_two_h_with_interval_15_min_and_complete()
{
var scheduler = new TestScheduler();
var pushCollection = Observable.Interval(TimeSpan.FromMinutes(15), scheduler)
.SelectMany(x=> Observable.Return(String.Format("NextData from Server #{0}", x)))
.Take(8);
mockObserver = new MockObserver<string>(scheduler);
pushCollection.Subscribe(mockObserver);
scheduler.Run();
}
}
На моей машине тест отработал за 0:00:00.26, а результат представлен ниже:На заметку. После знака @ (at) показано время, когда было вытолкнуто значение (((9000000000 tic/10000)msec)/1000)sec/60)min = 15 мин.
HotObservable/ColdObservable
Данные классы позволяют записать события, которые Observable будет “выталкивать”. Событие включает в себя время возникновения и значение. Классы HotObservable/ColdObservable ведут себя ожидаемым образом исходя из их названия. Первый начинает выталкивать значения независимо от подписки, второй ждет подписку и начитает публиковать значения. Рассмотрим пример использования HotObservable:[Test]
public void HotObservable_should_push_values_without_subscrible()
{
var scheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(scheduler);
var hotObservable = new HotObservable<int>(scheduler,
new Recorded<Notification<int>>(1, new Notification<int>.OnNext(1)),
new Recorded<Notification<int>>(2, new Notification<int>.OnNext(2)),
new Recorded<Notification<int>>(3, new Notification<int>.OnNext(3)),
new Recorded<Notification<int>>(4, new Notification<int>.OnNext(4)),
new Recorded<Notification<int>>(5, new Notification<int>.OnNext(5)),
new Recorded<Notification<int>>(6, new Notification<int>.OnNext(6)),
new Recorded<Notification<int>>(7, new Notification<int>.OnNext(7)),
new Recorded<Notification<int>>(8, new Notification<int>.OnNext(8)),
new Recorded<Notification<int>>(9, new Notification<int>.OnNext(9)),
new Recorded<Notification<int>>(10, new Notification<int>.OnCompleted())
);
scheduler.RunTo(4);
hotObservable.Subscribe(mockObserver);
scheduler.Run();
Assert.That(mockObserver.Count, Is.EqualTo(6));
Assert.That(mockObserver[0].Time, Is.EqualTo(5));
Assert.That(mockObserver[5].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
На строке 7 создается HotObservable и записывает туда последовательность событий с точным указанием времени “выталкивания”. Например, первое событие будет опубликовано в 1 тик времени со значением 1.
После записи последовательности, запускается выполнение до 4-го тика и выполняется подписка. В результирующей коллекции оказалось 6 значений из 10, т.к. первые четыре опубликовались “в никуда”.Результат:
Во-втором примере все точно также:
[Test]
public void ColdObservable_should_push_values_upon_subscrible()
{
var scheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(scheduler);
ar hotObservable = new ColdObservable<int>(scheduler,
new Recorded<Notification<int>>(1, new Notification<int>.OnNext(1)),
new Recorded<Notification<int>>(2, new Notification<int>.OnNext(2)),
new Recorded<Notification<int>>(3, new Notification<int>.OnNext(3)),
new Recorded<Notification<int>>(4, new Notification<int>.OnNext(4)),
new Recorded<Notification<int>>(5, new Notification<int>.OnNext(5)),
new Recorded<Notification<int>>(6, new Notification<int>.OnNext(6)),
new Recorded<Notification<int>>(7, new Notification<int>.OnNext(7)),
new Recorded<Notification<int>>(8, new Notification<int>.OnNext(8)),
new Recorded<Notification<int>>(9, new Notification<int>.OnNext(9)),
new Recorded<Notification<int>>(10, new Notification<int>.OnCompleted())
);
scheduler.RunTo(4);
hotObservable.Subscribe(mockObserver);
scheduler.Run();
Assert.That(mockObserver.Count, Is.EqualTo(10));
Assert.That(mockObserver[0].Time, Is.EqualTo(1));
Assert.That(mockObserver[9].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
за исключением того, что не зависимо от времени, при каждой подписке будет публиковаться вся коллекция полностью.Результат:
Для более удобного использования HotObservable/ColdObservable существыют ExtensionsMethods для TestScheduler:
public static class TestSchedulerExtensions
{
public static ColdObservable<T> CreateColdObservable<T>(this TestScheduler scheduler, params Recorded<Notification<T>>[] messages);
public static HotObservable<T> CreateHotObservable<T>(this TestScheduler scheduler, params Recorded<Notification<T>>[] messages);
}
Все классы, рассмотренные выше, находятся в сборке System.Reactive.Testing.dll, которая поставляется вместе с Reactive Extensions v1.0.2856.104.
В данной сборке есть еще много Помните:
"Documentation is like sex. If it is good, it is REALLY good, and if it is bad, it is better than nothing!" Gabe Helou





