Dzisiaj chciałbym zaprezentować klasę EventWaiter. Znalazłem ją w repozytorium Roslyn na GitHub i w niektórych testach okazała się przydatna. Załóżmy, że mamy klasę eksponującą jakieś zdarzenie:
class Sut { public event EventHandler Click; public void RaiseClickEvent() { Click?.Invoke(this,EventArgs.Empty); } }
Test zdarzenia mógłby wyglądać następująco:
[Test] [Timeout(2000)] public void Test1() { // arrange var manualResetEvent = new ManualResetEvent(false); var sut = new Sut(); sut.Click += (s, e) => { manualResetEvent.Set(); }; // act sut.RaiseClickEvent(); // assert manualResetEvent.WaitOne(); Assert.Pass(); }
Źródło klasy EventWaiter można znaleźć np. tutaj:
https://github.com/dotnet/roslyn/blob/master/src/Test/Utilities/Shared/FX/EventWaiter.cs
Kod:
public sealed class EventWaiter : IDisposable { private readonly ManualResetEvent _eventSignal = new ManualResetEvent(false); private Exception _capturedException; /// <summary> /// Returns the lambda given with method calls to this class inserted of the form: /// /// try /// execute given lambda. /// /// catch /// capture exception. /// /// finally /// signal async operation has completed. /// </summary> /// <typeparam name="T">Type of delegate to return.</typeparam> /// <param name="input">lambda or delegate expression.</param> /// <returns>The lambda given with method calls to this class inserted.</returns> public EventHandler<T> Wrap<T>(EventHandler<T> input) { return (sender, args) => { try { input(sender, args); } catch (Exception ex) { _capturedException = ex; } finally { _eventSignal.Set(); } }; } /// <summary> /// Use this method to block the test until the operation enclosed in the Wrap method completes /// </summary> /// <param name="timeout"></param> /// <returns></returns> public bool WaitForEventToFire(TimeSpan timeout) { var result = _eventSignal.WaitOne(timeout); _eventSignal.Reset(); return result; } /// <summary> /// Use this method to block the test until the operation enclosed in the Wrap method completes /// </summary> /// <param name="timeout"></param> /// <returns></returns> public void WaitForEventToFire() { _eventSignal.WaitOne(); _eventSignal.Reset(); return; } /// <summary> /// IDisposable Implementation. Note that this is where we throw our captured exceptions. /// </summary> public void Dispose() { _eventSignal.Dispose(); if (_capturedException != null) { throw _capturedException; } } }
Dzięki temu wrapperowi, nasz kod może wyglądać teraz tak:
[Test] [Timeout(2000)] public void Test1a() { // arrange var waiter=new EventWaiter(); var sut = new Sut(); sut.Click += waiter.Wrap<EventArgs>((sender, args) => { Assert.Pass()}); // act sut.RaiseClickEvent(); // assert waiter.WaitForEventToFire(); }
Powyższy przykład jest prosty. W praktyce musimy wciąć pod uwagę różne scenariusze – np. gdy wyjątek jest wyrzucany albo musimy sprawdzić konkretne parametry zdarzenia. W takich sytuacjach, powyższy wrapper jest przydatny.