Asynchronous Programming - Event Testing
Sometimes you have to write tests for events, and it is inconvenient to do this - very quickly additional methods and fields begin to multiply. I also want to tell how to test events in C #.
To begin with an example. I have an API that asynchronously downloads a webpage. To get the page, I subscribe to the event [ 1 ] and thereby create another method in which my assertions actually live. Naturally, the test method itself has to be blocked, because otherwise, test runner just exits without waiting for my callback.
Unfortunately, this approach does not take into account the situation in which the test "freezes" and thereby slows down the testing process. To do this, you can add a timeout for the call,
In order to avoid multiplying the number of methods and variables in our test fixture, it would be useful to place the “asynchronous tester” itself in a separate class that could be used as a manager. [ 2 ]
What does this tester do? Firstly, it caches the callback method that needs to be called for post hoc tests. Further, it provides a method
As for the test itself, now it looks like this:
This is certainly not the most readable test in the world, but at least it does not produce additional fields and methods in the class body. Testing events, just like testing pairs,
Notes
To begin with an example. I have an API that asynchronously downloads a webpage. To get the page, I subscribe to the event [ 1 ] and thereby create another method in which my assertions actually live. Naturally, the test method itself has to be blocked, because otherwise, test runner just exits without waiting for my 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/>
Unfortunately, this approach does not take into account the situation in which the test "freezes" and thereby slows down the testing process. To do this, you can add a timeout for the call,
WaitOne()
but then we must also test the likelihood that the callback will not be called at all. To do this, we have to add another type variable bool
. Here is the result: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/>
In order to avoid multiplying the number of methods and variables in our test fixture, it would be useful to place the “asynchronous tester” itself in a separate class that could be used as a manager. [ 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/>
What does this tester do? Firstly, it caches the callback method that needs to be called for post hoc tests. Further, it provides a method
Wait()
that is delegated to ours ManualResetEvent
and thereby allows us to wait some time until the event handler is called. As soon as it is called, we immediately launch the final tests. And then - a little trick - we implement IDisposable()
and in the method Dispose()
check whether the handler was called. Since assert interrupts execution, the subsequent one EndInvoke()
will only be called if it is appropriate. As for the test itself, now it looks like this:
[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/>
This is certainly not the most readable test in the world, but at least it does not produce additional fields and methods in the class body. Testing events, just like testing pairs,
BeginXxx()/EndXxx()
is a simple task, since it always predictably contains two elements - a call and a handler, a start and an end. ■ Notes
- ↑ In this case, I'm testing a class
MicrosoftSubscription.Sync.WebDataProvider
- this is part of the Syndicated Client Experiences SDK , a framework that uses the well-known photoSuru application. - ↑ This approach was suggested by a certain vansickle in the comments on my blog page .