Sunday 20 February 2011

Problems Unit-Testing Quartz.NET's IJob.Execute Method

I've been using an open source library at work called Quartz.NET which is an Enterprise Job Scheduler for the .NET platform. Quartz.NET is modeled after the popular Quartz Scheduler for the Java platform and shares many similar elements. I needed a scheduler for a variety of reasons but it is mostly used to fire "jobs" at specified times that perform some work like downloading a file via FTP and storing it to disk.

I've been using Quartz.NET version 1.0.3 which has the following IJob interface and can be used to implement your own custom "jobs" that perform some task:

namespace Quartz
{
     public interface IJob 
     {
          void Execute(JobExecutionContext context);
     }
}

The Execute method looks fairly simple and non-problematic until you realize that it is the single entry point that Quartz.NET has into your own application code (i.e. the scheduler calls the implemented Execute method on your custom jobs when a trigger fires). Problems start to arise if you ever want to thoroughly unit-test your custom jobs as the JobExecutionContext parameter passed in is a concrete object and not an interface. A previous post of mine has already mentioned my theory of software development with bugs so in that light let me explain further.

In order to effectively unit-test you need to isolate the method under test and test ONLY the logic contained within that singular method (not any other class's methods that may be called from the method under test). Misko Hevery of Google has a good presentation on what makes code hard to test and the above Quartz.NET interface code makes it hard to pass in a completely clean mock object as the parameter since the JobExecutionContext parameter is concrete and cannot be completed mocked (unless all its methods are virtual which they are not).

I've been using RhinoMocks to easily mock up my own interfaced objects, set expectations on what methods should be called on those mocked up objects and then asserting that the methods were in fact called (see this post for details). When RhinoMocks is used to mock up an interface or class it essentially wraps a proxy object around the interface, abstract class or concrete class you give it. It works great for interfaces but it can only override interface methods, delegates or virtual methods. Therefore if you try to mock up any class with non-virtual methods and then need to set expectations on those non-virtual methods you're out of luck (See these RhinoMock limitations). Most of the methods on the JobExecutionContext are virtual but there are some key ones related to the next, previous and current fire times which are not virtual and therefore cannot be set by RhinoMocks.

After understanding this problem I posted a question to the Quartz.NET Google Group asking why the JobExecutionContext parameter is not an interface. As it turns out the next version of Quartz.NET will use an interface for this parameter instead of a concrete type.

So there are 3 options that I can think of when using the Quartz.NET IJob interface for your own custom jobs (Some are more geared towards fully unit-testing the logic contained within your custom jobs):
  1. Continue using the Quartz.NET 1.0.3 library and either avoid testing your custom job logic completely by ignoring the next, previous and current fire time properties OR create a concrete JobExecutionContext object within your test code and pass that into your custom job's Execute method. I would avoid the first approach as you're then missing test coverage but the second approach is not ideal as you're not isolating your method logic and therefore a single change to one method may end up breaking multiple disparate unit-tests which take a while to determine whats wrong and fix.
  2. Wait for the next version of the Quartz.NET library which has an interfaced parameter for the IJob.Execute() method. This should be released sometime in 2011.
  3. Modify the Quartz.NET 1.0.3 open source library yourself so that the JobExecutionContext parameter on the IJob.Execute() method is an interface instead of a concrete type. This isn't an ideal solution as you now have to manage your own 3rd party library but if you can't wait for the next Quartz.NET release but still need the convenience of an interfaced JobExecutionContext parameter this is your only real option.

No comments: