Асинхронное программирование — тестирование событий

    Иногда приходится писать тесты для событий, и делать это неудобно – очень быстро начинают плодиться дополнительные методы и поля. О том, как тестировать события в C# я и хочу рассказать.


    Для начала пример. У меня есть API, который асинхронно скачивает веб-страницу. Чтобы получить страницу, я подписываюсь на событие[1] и тем самым создаю еще один метод, в котором собственно живут мои ассерты. Естественно, сам тестовый метод при этом приходится блокировать, т.к. иначе test runner просто выйдет из него, не дождавшись моего callback’а.

    [TestFixture]<br/>
    public class MyTests<br/>
    {<br/>
      private ManualResetEvent waitHandle;<br/>
      [Test] <br/>
      public void TestAsyncPageDownloading()<br/>
      {<br/>
        waitHandle = new ManualResetEvent(false);<br/>
        ⋮<br/>
        wdp.GetWebDataCompleted += wdp_GetWebDataCompleted;<br/>
        wdp.GetWebDataAsync(new Uri("http://nesteruk.org/blog"), new object());<br/>
        waitHandle.WaitOne(); // ждём пока выполнятся assert'ы
      }<br/>
      void wdp_GetWebDataCompleted(object sender, GetWebDataCompletedEventArgs e)<br/>
      {<br/>
        StreamReader sr = new StreamReader(e.Stream);<br/>
        string s = sr.ReadToEnd();<br/>
        // тест ниже будет вызван test runner'ом
        Assert.Contains(s, "Dmitri""My webpage should have my name.");<br/>
        waitHandle.Set(); // разблокировка теста
      }<br/>
    }<br/>

    К сожалению, такой подход не учитывает ситуацию в которой тест «зависает» и тем самым тормозит процесс тестирования. Для этого можно добавить тайм-аут для вызова WaitOne() но тогда мы должны также протестировать вероятность того, что callback вообще не будет вызван. Для этого нам придется добавить еще одну переменную типа bool. Вот что получится:

    private bool callbackInvoked;<br/>
    ⋮<br/>
    [Test]<br/>
    public void TestAsyncPageDownloading()<br/>
    {<br/>
      waitHandle = new ManualResetEvent(false);<br/>
      wdp.GetWebDataCompleted += wdp_GetWebDataCompleted;<br/>
      wdp.GetWebDataAsync(new Uri("http://nesteruk.org/blog"), new object());<br/>
      waitHandle.WaitOne(5000); // ждём максимум 5 секунд пока выполнятся assert'ы
      Assert.IsTrue(callbackInvoked, "Callback method was never called");<br/>
    }<br/>
    void wdp_GetWebDataCompleted(object sender, GetWebDataCompletedEventArgs e)<br/>
    {<br/>
      callbackInvoked = true; // информируем всех что метод был вызван
      StreamReader sr = new StreamReader(e.Stream);<br/>
      string s = sr.ReadToEnd();<br/>
      // тест ниже будет вызван test runner'ом
      Assert.Contains(s, "Dmitri""My webpage should have my name.");<br/>
      waitHandle.Set(); // разблокировка теста
    }<br/>

    Дабы избежать преумножения количества методов и переменных в нашей test fixture, было бы полезно вынести сам «асинхронный тестировщик» в отдельный класс, который можно было бы использовать в качестве менеджера.[2]

    public class EventTester<SenderType, ArgumentType> : IDisposable<br/>
    {<br/>
      private readonly ManualResetEvent waitHandle;<br/>
      private readonly Action<SenderType, ArgumentType> postHocTests;<br/>
      private bool called;<br/>
      private IAsyncResult waitToken;<br/>
      public EventTester(Action<SenderType, ArgumentType> postHocTests)<br/>
      {<br/>
        waitHandle = new ManualResetEvent(false);<br/>
        this.postHocTests = postHocTests;<br/>
      }<br/>
      public void Handler(SenderType sender, ArgumentType args)<br/>
      {<br/>
        waitHandle.Set();<br/>
        waitToken = postHocTests.BeginInvoke(sender, args, nullnull);<br/>
      }<br/>
      public void Wait(int mullisecondsTimeout)<br/>
      {<br/>
        called = waitHandle.WaitOne(mullisecondsTimeout);<br/>
      }<br/>
      public void Dispose()<br/>
      {<br/>
        Assert.IsTrue(called, "The event was never handled");<br/>
        postHocTests.EndInvoke(waitToken);<br/>
      }<br/>
    }<br/>

    Что делает этот тестировщик? Во-первых он кэширует колбэк-метод, который нужно вызвать для post hoc-тестов. Далее, он предоставляет метод Wait(), который делегируется нашему ManualResetEvent и тем самым позволяет нам подождать сколько-то времени пока будет вызван обработчик события. Как только оно вызвано – мы тут же запускаем финальные тесты. А дальше – небольшой трюк – мы реализуем IDisposable() и в методе Dispose() проверяем, вызывался ли обработчик. Поскольку assert прерывает исполнение, последующий EndInvoke() будет вызван только в том случае, когда он уместен.

    Что касается самого теста, то теперь он выглядит вот так:

    [Test]<br/>
    public void BetterAsyncTest()<br/>
    {<br/>
      using (var eventTester = new EventTester<object, GetWebDataCompletedEventArgs>(<br/>
        (o, args) =><br/>
        {<br/>
          StreamReader sr = new StreamReader(args.Stream);<br/>
          string s = sr.ReadToEnd();<br/>
          Assert.Contains(s, "Dmitri""My webpage should have my name.");<br/>
        }))<br/>
      {<br/>
        wdp.GetWebDataCompleted += eventTester.Handler;<br/>
        wdp.GetWebDataAsync(new Uri("http://nesteruk.org/blog"), new object());<br/>
        eventTester.Wait(5000);<br/>
      }<br/>
    }<br/>

    Это конечно не самый читабельный тест в мире, но по крайней мере он не плодит дополнительные поля и методы в теле класса. Тестирование событий, ровно как и тестирование пар BeginXxx()/EndXxx() – задача простая, так как в ней всегда предсказуемо фигурируют два элемента – вызов и обработчик, начало и конец. ■

    Заметки
    1. В данном случае я тестирую класс MicrosoftSubscription.Sync.WebDataProvider – это часть Syndicated Client Experiences SDK, фреймворка который использует всем известное приложение photoSuru.
    2. Этот подход был предложен неким vansickle в комментариях на странице моего блога.
    Поделиться публикацией

    Похожие публикации

    Комментарии 7

      +4
      Круто, только не thread safe.
      Так еще круче:

      public class EventTester: IDisposable
      {
      private readonly Action _postHocTests;

      private readonly ManualResetEvent _waitHandle;

      private bool _called;

      private IAsyncResult _waitToken;

      public EventTester(Action postHocTests)
      {
      _waitHandle = new ManualResetEvent(false);
      _postHocTests = postHocTests;
      }

      #region IDisposable Members

      public void Dispose()
      {
      lock (_waitHandle)
      {
      Assert.IsTrue(_called, «The event was never handled»);
      _postHocTests.EndInvoke(_waitToken);
      }
      }

      #endregion

      public void Handler(TSender sender, TArgument args)
      {
      lock (_waitHandle)
      {
      _waitHandle.Set();
      _waitToken = _postHocTests.BeginInvoke(sender, args, null, null);
      }
      }

      public void Wait(int mullisecondsTimeout)
      {
      _called = _waitHandle.WaitOne(mullisecondsTimeout);
      }
      }
        0
        Хабр сожрал дженерик парамеры :(
          0
          Слышать это с уст веб программера смешно ;)
            0
            Ну блин, расслабился понимаешь. Понаделали везде авто экранирования ;)
        0
        А зачем тестировать MicrosoftSubscription.Sync.WebDataProvider? Не уверены, что он работает?
          0
          по моему не важно, что, главное как тут тестируется. можно ведь другой код так тестить…
          0
          В сторону IOCP не пробовали смотреть?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое