воскресенье, 13 марта 2011 г.

Реактивное тестирование

Rx принес некоторые интересные проблемы в сообщество TDD, ибо производить тестирование асинхронной модели задача не из простых, и требует иной подход нежели тестирование синхронного кода. В данной статье будут рассмотрены способы тестирования Rx модели.

Запись событий и анализ

Класс 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 - источник события
Источник события представляет абстрактный класс Notification<> и его три реализации OnCompleted, OnError, OnNext. Базовый класс также имеет два важный свойства:
  • 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 эмулирует работу планировщика.
Планировщики можно разделить на два вида их применения:
  1. Выполнение работы подписки (SubscribeOn())
  2. Публикация результатов работы (ObserveOn())
TestScheduler применяет концепцию виртуального планирования и позволяет контролировать и управлять временем при выполнении операции. Концепцию виртуального планирования можно представить в виде очереди операций, которые необходимо выполнить в определенное время. Если операции запланирована на одно и тоже время, они будут выполнены последовательно и помечены одинаковым временным штампом. При использовании TestSheduler, можно приказать ему выполнить все запланированные операции TestSheduler.Run(), или выполнить операции до определенного момента времени, которое передается в качестве аргумента методу TestSheduler.RunTo(TimeInTicks). В предыдущем примере планировщику приказано выполнить все операции (строка 13) не дожидаясь задержки в 1 миллисекунду, причем в записи будет указано, что операция выполнилась ровно через одну миллисекунду после подписки. Рассмотрим пример, где запланированные операции выполняются не все сразу, а до определенного момента времени:
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. В данной сборке есть еще много сюрпризов интересных методов, например, проверка на равенство двух 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

0 коммент.:

Отправить комментарий

Newer Posts Older Posts