Модульное тестирование события с использованием Reactive Extensions - PullRequest
6 голосов
/ 12 октября 2010

Я использую Reactive Extensions для .NET (Rx) , чтобы выставлять события как IObservable<T>. Я хочу создать модульный тест, в котором я утверждаю, что происходит определенное событие. Вот упрощенная версия класса, который я хочу протестировать:

public sealed class ClassUnderTest : IDisposable {

  Subject<Unit> subject = new Subject<Unit>();

  public IObservable<Unit> SomethingHappened {
    get { return this.subject.AsObservable(); }
  }

  public void DoSomething() {
    this.subject.OnNext(new Unit());
  }

  public void Dispose() {
    this.subject.OnCompleted();
  }

}

Очевидно, что мои настоящие занятия более сложные. Моя цель состоит в том, чтобы проверить, что выполнение некоторых действий с тестируемым классом приводит к последовательности событий, сигнализируемой на IObservable. К счастью, классы, которые я хочу протестировать, реализуют IDisposable и вызывают OnCompleted для субъекта, когда объект расположен, что делает тестирование намного проще.

Вот как я тестирую:

// Arrange
var classUnderTest = new ClassUnderTest();
var eventFired = false;
classUnderTest.SomethingHappened.Subscribe(_ => eventFired = true);

// Act
classUnderTest.DoSomething();

// Assert
Assert.IsTrue(eventFired);

Использование переменной для определения того, инициировано ли событие, не так уж плохо, но в более сложных сценариях я могу захотеть проверить, запускается ли определенная последовательность событий. Возможно ли это без простой записи событий в переменных и последующего выполнения утверждений для переменных? Будучи способным использовать свободный LINQ-подобный синтаксис для создания утверждений на IObservable, можно надеяться, что тест станет более читабельным.

Ответы [ 3 ]

13 голосов
/ 29 октября 2010

Этот ответ был обновлен до текущей версии 1.0 Rx.

Официальная документация все еще скудна, но Тестирование и отладка наблюдаемых последовательностей в MSDN - хорошая отправная точка.

Тестовый класс должен быть производным от ReactiveTest в пространстве имен Microsoft.Reactive.Testing. Тест основан на TestScheduler, который предоставляет виртуальное время для теста.

Метод TestScheduler.Schedule можно использовать для постановки задач в определенные моменты (тики) в виртуальном времени. Тест выполнен TestScheduler.Start. Это вернет ITestableObserver<T>, который можно использовать, например, для утверждения с помощью класса ReactiveAssert.

public class Fixture : ReactiveTest {

  public void SomethingHappenedTest() {
    // Arrange 
    var scheduler = new TestScheduler();
    var classUnderTest = new ClassUnderTest();

    // Act 
    scheduler.Schedule(TimeSpan.FromTicks(20), () => classUnderTest.DoSomething());
    var actual = scheduler.Start(
      () => classUnderTest.SomethingHappened,
      created: 0,
      subscribed: 10,
      disposed: 100
    );

    // Assert
    var expected = new[] { OnNext(20, new Unit()) };
    ReactiveAssert.AreElementsEqual(expected, actual.Messages);
  }

}

TestScheduler.Schedule используется для планирования вызова на DoSomething в момент времени 20 (измеряется в тиках).

Затем TestScheduler.Start используется для выполнения фактического теста на наблюдаемой SomethingHappened. Время жизни подписки контролируется аргументами вызова (снова измеряется в тиках).

Наконец, ReactiveAssert.AreElementsEqual используется для проверки того, что OnNext был вызван во время 20, как и ожидалось.

Тест проверяет, что вызов DoSomething немедленно запускает наблюдаемое SomethingHappened.

5 голосов
/ 14 октября 2010

Этот вид тестирования для наблюдаемых будет неполным.Совсем недавно команда RX опубликовала планировщик тестов и некоторые расширения (которые, кстати, они используют внутри для тестирования библиотеки).Используя их, вы можете не только проверить, произошло ли что-то или нет, но и убедиться, что сроки и порядок верны.В качестве бонуса планировщик тестов позволяет вам запускать тесты в «виртуальном времени», поэтому тесты запускаются мгновенно, независимо от того, какие большие задержки вы используете внутри.

Джеффри ван Гог из команды RX опубликовал статью о том, как проводить такого рода тестирование.

Приведенный выше тест с использованием упомянутого подхода будет выглядеть следующим образом:

    [TestMethod]
    public void SimpleTest()
    {
        var sched = new TestScheduler();
        var subject = new Subject<Unit>();
        var observable = subject.AsObservable();

        var o = sched.CreateHotObservable(
            OnNext(210, new Unit())
            ,OnCompleted<Unit>(250)
            );
        var results = sched.Run(() =>
                                    {
                                        o.Subscribe(subject);
                                        return observable;
                                    });
        results.AssertEqual(
            OnNext(210, new Unit())
            ,OnCompleted<Unit>(250)
            );
    }:

РЕДАКТИРОВАТЬ: Вы также можете позвонить.OnNext (или какой-то другой метод) неявно:

        var o = sched.CreateHotObservable(OnNext(210, new Unit()));
        var results = sched.Run(() =>
        {
            o.Subscribe(_ => subject.OnNext(new Unit()));
            return observable;
        });
        results.AssertEqual(OnNext(210, new Unit()));

Моя точка зрения - в простейших ситуациях вам нужно просто убедиться, что событие запущено (например, вы проверяете, где работает правильно).Но тогда, когда вы прогрессируете в сложности, вы начинаете тестирование на время, или завершение, или что-то еще, что требует виртуального планировщика.Но природа теста с использованием виртуального планировщика, в отличие от «обычных» тестов, заключается в тестировании всего наблюдаемого за один раз, а не «атомарных» операций.

Так что, вероятно, вам придется переключиться на виртуальный планировщик где-нибудь в будущем - почему бы не начать с него в начале?

PS Кроме того, вам придется прибегнуть к другой логике длякаждый тестовый случай - например, у вас будет совсем другое наблюдение для проверки того, что что-то не произошло, противоположное тому, что произошло.

0 голосов
/ 13 октября 2010

Не уверен, что лучше говорить, но это поможет, не вводя переменную.

var subject = new Subject<Unit>();
subject
    .AsObservable()
    .Materialize()
    .Take(1)
    .Where(n => n.Kind == NotificationKind.OnCompleted)
    .Subscribe(_ => Assert.Fail());

subject.OnNext(new Unit());
subject.OnCompleted();
...