2010-10-12 58 views
6

我使用Reactive Extensions for .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進行斷言將有希望使測試更具可讀性。

+1

順便說一句,我認爲有一個變量是非常好的。上面的代碼很容易閱讀,這是最重要的。 @ PL的答案很好,也很優雅,但是你必須努力去理解正在發生什麼......也許把它變成一個擴展FailIfNothingHappened() – 2010-10-15 15:33:56

+0

@Sergey Aldoukhov:我同意,但是PL的答案學會了我如何使用'物化'來推測我的IObservable行爲。對於使用變量來捕捉髮生的事情的更復雜的測試可能更難理解。另外,根據您的建議創建擴展可能會更容易理解正在發生的事情。 – 2010-10-15 16:01:32

+0

我編輯了我的問題,使其更清楚我想要什麼。 – 2010-10-29 11:59:03

回答

11

此答案已更新至現在發佈的Rx版本1.0。

官方文檔仍然不足,但MSDN上的Testing and Debugging Observable Sequences是一個很好的起始位置。

測試類別應該來自Microsoft.Reactive.Testing命名空間中的ReactiveTest。該測試基於爲測試提供虛擬時間的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用於調度在時間20(在蜱測量)到DoSomething的呼叫。

然後TestScheduler.Start用於在可觀察的SomethingHappened上執行實際測試。訂閱的生命週期由調用的參數控制(再次以tick爲單位)。

最後ReactiveAssert.AreElementsEqual用於驗證OnNext在預期的時間20被調用。

該測試驗證呼叫DoSomething立即觸發可觀察的SomethingHappened

0

不確定更流利,但這不會引入變量。

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(); 
+1

我相當肯定這不起作用,在訂閱中聲明經常導致奇怪的事情發生,並且測試仍然通過。 – 2010-10-13 17:29:06

+0

要測試失敗,您應該使用OnNext調用註釋掉該行。 – 2010-10-13 17:39:47

+1

只是一點;在這個例子中,AsObservable()沒有任何用處。 – 2012-06-17 07:31:45

3

這種觀察對象的測試是不完整的。就在最近,RX團隊發佈了測試調度程序和一些擴展(它們在內部用於測試庫)。 使用這些,你不僅可以檢查是否發生了某些事情,還可以確保時間和訂單是正確的。作爲獎勵,測試計劃程序允許您在「虛擬時間」內運行測試,因此無論您在內部使用多大的延遲,測試都能立即運行。

傑弗裏·梵高從RX隊published an article on how to do such kind of testing.

以上測試,使用上述方法,將是這樣的:

[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())); 

我的觀點是 - 在最簡單的情況下,你只需要確保該事件被觸發(FE您檢查您在哪裏運行的C orrectly)。但隨着您複雜性的進步,您將開始測試計時,完成或需要虛擬調度程序的其他內容。但是,與「正常」測試相反,使用虛擬調度程序進行測試的本質是一次測試整個觀察值,而不是「原子」操作。

因此,您可能需要切換到虛擬調度程序的某個地方 - 爲什麼不從頭開始?

P.S.此外,您將不得不爲每個測試用例採取不同的邏輯 - f.e.你會有非常不同的觀察結果,因爲測試發現事情並沒有發生與事情相反的事情。

+2

這種方法似乎非常適合測試Rx本身。但是,我不想綜合'OnNext'調用。相反,我想斷言在我正在測試的類中調用方法實際上是導致在「IObservable」上調用「OnNext」。 – 2010-10-15 09:40:12