Saturday, August 13, 2005

Testing Events in C#

As I previously mentioned in Firing Silver Bullets I like to try things out in excess. Recently, I've been using events for Error Handling between layers, Separation of Presentation Logic, and just about anything that seemed like it might be a fit.

The most common argument you hear against events is that they are hard to test. Never being shy about a challenge I set out to dismiss this myth. My recent event experience has been in both C# 2.0 and 1.1. Depending on which version of C# I'm using the tests differ slightly.

Assume a simple class called PositiveNumber that takes an int in it's constructor and fires an Invalid event when Validate() is called if the constructor arg is not positive.

Example C# 1.1
[Test]
public void PositiveNumberDoesNotFireInvalidIfNumberIsPositive()
{
PositiveNumber num = new PositiveNumber(1);
num.Invalid+=new ValidationHandler(AssertFail);
num.Validate();
}
public void AssertFail()
{
Assert.Fail();
}

That's simple enough for me; however, testing that the event does occur is less straight forward. One option is to declare a class level bool that is set to false in the test and then set to true by the event handler. After the event is fired the bool can be tested for true. I've never been a fan of class variables in tests since they feel like global variables in a procedural application. Therefore, I actually prefer throwing a success exception (yes, I did say that).
[Test, ExpectedException(typeof(ApplicationException),"Success")]
public void PositiveNumberDoesFireInvalidIfNumberIsNotPositive()
{
PositiveNumber num = new PositiveNumber(0);
num.Invalid+=new ValidationHandler(ThrowSuccess);
num.Validate();
}
public void ThrowSuccess()
{
throw new ApplicationException("Success");
}

Perhaps a better exception than ApplicationException could be used, but you get the point. You hate it? You never imagined an exception could indicate success and "Exceptions are only for exceptional situations". Yeah, I get all that, but what's more clear than 4 lines of code that show expected behavior for an event? Read it a few more times and try to think of something more clear. Let me know if you find it.

Example C# 2.0
[Test]
public void PositiveNumberDoesNotFireInvalidIfNumberIsPositive()
{
PositiveNumber num = new PositiveNumber(1);
num.Invalid += delegate { Assert.Fail(); }
num.Validate();
}

Not much different, but Anonymous Methods do make it a bit cleaner. With the addition of Anonymous Methods I abandon the ThrowSuccess method. I could just throw the exception in the Anonymous Method; however, I can now declare the bool in the method and access it from the Anonymous Method. I'm not sure which I prefer more, but my teammates seem to prefer this method.
[Test]
public void PositiveNumberDoesFireInvalidIfNumberIsNotPositive()
{
bool methodCalled = false;
PositiveNumber num = new PositiveNumber(0);
num.Invalid += delegate { methodCalled = true; }
num.Validate();
Assert.IsTrue(methodCalled);
}

Testing the object that raises the events is fairly easy; however, testing the observers of these events can seem tough at first glance. In testing View Observer we used 3 different approaches. I'll detail those in the next few days.

2 comments:

  1. Thanx for explaining how to test events. I've grappled with the concept of testing events now for a while and this post helped me get a good understanding of the how-to...

    ReplyDelete
  2. Anonymous11:38 AM

    Hi there!

    I have found a more elegant method on testing the events. The solution is based on the Monitor class.

    The thing you do is just make the application wait (with some time out) until the event occurs. In case the timeout happens you raise an exception. That is simple, but you don't need to consider TRUE as FALSE and vice versa.

    You do:
    1. Add the following method to the testing class

    public void WaitResourceToUnlock(int timeout)
    {
    lock(sync)
    {
    Monitor.Wait(sync, timeout);
    }
    }


    and the parameter
    object sync = new object();
    2. In your test write

    WaitResourceToUnlock(2000);
    Assert.IsNotNull(testableobject);

    3. Add the event handler
    private void SomeEvent(object sender, Args e)
    {
    testableobject = new testableobject();
    Monitor.Pulse(sync);
    }


    This approach works great to me :)

    Cheers!

    ReplyDelete

Note: Only a member of this blog may post a comment.