Иногда приходится писать тесты для событий, и делать это неудобно – очень быстро начинают плодиться дополнительные методы и поля. О том, как тестировать события в C# я и хочу рассказать.
Для начала пример. У меня есть API, который асинхронно скачивает веб-страницу. Чтобы получить страницу, я подписываюсь на событие[1] и тем самым создаю еще один метод, в котором собственно живут мои ассерты. Естественно, сам тестовый метод при этом приходится блокировать, т.к. иначе test runner просто выйдет из него, не дождавшись моего callback’а.
К сожалению, такой подход не учитывает ситуацию в которой тест «зависает» и тем самым тормозит процесс тестирования. Для этого можно добавить тайм-аут для вызова
Дабы избежать преумножения количества методов и переменных в нашей test fixture, было бы полезно вынести сам «асинхронный тестировщик» в отдельный класс, который можно было бы использовать в качестве менеджера.[2]
Что делает этот тестировщик? Во-первых он кэширует колбэк-метод, который нужно вызвать для post hoc-тестов. Далее, он предоставляет метод
Что касается самого теста, то теперь он выглядит вот так:
Это конечно не самый читабельный тест в мире, но по крайней мере он не плодит дополнительные поля и методы в теле класса. Тестирование событий, ровно как и тестирование пар
Заметки
Для начала пример. У меня есть 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, null, null);<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()
– задача простая, так как в ней всегда предсказуемо фигурируют два элемента – вызов и обработчик, начало и конец. ■Заметки
- ↑ В данном случае я тестирую класс
MicrosoftSubscription.Sync.WebDataProvider
– это часть Syndicated Client Experiences SDK, фреймворка который использует всем известное приложение photoSuru. - ↑ Этот подход был предложен неким vansickle в комментариях на странице моего блога.