Testing Retries in Quartz.NET
March 23, 2015 2 Comments
This is the second in a series of posts.
In the last post I showed you the moving parts for persistent retries with Quartz.NET. Now it’s time to bring them all together.
With a little trick it is quite easy to run the actual Quartz.NET scheduler in a unit test. My current testing framework of choice is xUnit. So that’s what I will use in my code.
[Fact] public void Should_Try_3_Times_And_Then_Give_Up() { ISchedulerFactory factory = new StdSchedulerFactory(); IScheduler scheduler = factory.GetScheduler(); AlwaysFails alwaysFails = new AlwaysFails(); IJob decoratedJob = new EnsureJobExecutionExceptionDecorator(alwaysFails); var jobFactory = new Mock<IJobFactory>(); jobFactory .Setup( jf => jf.NewJob(It.IsAny<TriggerFiredBundle>(), It.IsAny<IScheduler>())) .Returns(decoratedJob); ManualResetEvent reset = new ManualResetEvent(false); IRetrySettings settings = new InMemoryRetrySettings { BackoffBaseInterval = 250.Milliseconds(), MaxRetries = 2 }; IRetryStrategy sut = new ExponentialBackoffRetryStrategy(settings); IRetryStrategy retryStrategy = new UnfreezeWhenJobShouldNotRunAgain(sut, reset); IJobListener retryListener = new RetryJobListener(retryStrategy); scheduler.ListenerManager.AddJobListener( retryListener, GroupMatcher<JobKey>.AnyGroup()); scheduler.JobFactory = jobFactory.Object; ITrigger trigger = TriggerBuilder .Create() .StartNow() .WithSimpleSchedule( x => { x.WithIntervalInSeconds(1); x.WithRepeatCount(0); }) .WithIdentity("always", "fails") .Build(); IJobDetail job = JobBuilder .Create<AlwaysFails>() .WithIdentity("always", "fails") .Build(); scheduler.ScheduleJob(job, trigger); scheduler.Start(); scheduler.ResumeAll(); reset.WaitOne(15.Seconds()); Assert.Equal(3, alwaysFails.Counter); }
We start by setting up the SchedulerFactory and use it to get us a live Quartz.NET scheduler.
For this test I want to be able to tell that scheduler how it should create instances of my job class. I usually use a DI container to implement that factory but for the purpose of this test I use a mock object created with Moq.
Whenever the scheduler asks for an instance of the AlwaysFails
job the factory returns a canned instance I created as part of my test setup. I explain the EnsureJobExecutionExceptionDecorator
in another post.
Next I plug the actual RetryJobListener
together and set it up to listen to all jobs using a catch-all matcher.
Then I configure my job to run immediately. Note that you can instruct the scheduler to run your job repeatedly but that is not what I want here. Under ideal circumstances the job should run once and be done with it. Only in case of failure do I want to run it again. And as another note: You can set a flag on the JobExecutionException that will cause the scheduler to retry immediately. But again: Not what I want.
When you start the scheduler it will create a background thread and do all its work there. The test method will just continue and exit before anything meaningful happens. Thus I need to freeze the test execution thread for a little while. This is what the ManualResetEvent is used for. To allow the test to continue as soon as the retry strategy says we are done trying I use another decorator. This one wraps the retry strategy. Please see the aforementioned post for further details.
In this test I use a large part of the Quartz.NET infrastructure. It’s not quite a system test but close enough for me at the moment. It can be run along with my unit tests because everything is done in-memory and it really doesn’t take that long to finish.
In the final post of this series we will make an excursion into the realm of composable software systems. Stay tuned!
Pingback: Quartz and other gems | Outlawtrail - .NET Development
Pingback: Quartz.NET meets Design Patterns | Outlawtrail - .NET Development