Timer Evaluation: Unit Testing Timer configurations outside of execution


#1

some reusable code for anyone wanting to do timer evaluations / QA checks on the configurations of your timers in your BPMN

Here is a Groovy trait that can be applied to a BpmnModelInstance:

trait bpmnTimers{
    // reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
    // https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events

    Collection<TimerEventDefinition> getTimers(){
        BpmnModelInstance model = (BpmnModelInstance)this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        return timerEventDefinitions
    }

    Map<String,Date> evaluateTimers() {
        BpmnModelInstance model = (BpmnModelInstance) this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        Map<String, Date> timers = [:]
        timerEventDefinitions.each { timer ->
            Map<String, Date> timerEval = evaluateTimer(timer)
            timers.putAll(timerEval)
        }
        return timers
    }

    Map<String, Date> evaluateTimer(TimerEventDefinition timer){
        Map<String,String> timerInfo = getTimerValue(timer)
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }

        switch (timerInfo) {
            case { it.type == 'date'}:
                DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
                Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)
                return [(activityId) : dueDate]
            case {it.type == 'cycle'}:
                CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
                Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)
                return [(activityId) : cycleDueDate]
            case {it.type == 'duration'}:
                DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
                Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)
                return [(activityId) : durationDueDate]
            default:
                throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
        }
    }

    Map<String, String> getTimerValue(TimerEventDefinition timer) {
        if (timer.getTimeDate() != null) {
            return [('type'):'date',
                    ('value'): timer.getTimeDate().getRawTextContent()]
        } else if (timer.getTimeCycle() != null) {
            return [('type'):'cycle',
                    ('value'): timer.getTimeCycle().getRawTextContent()]
        } else if (timer.getTimeDuration() != null) {
            return [('type'):'duration',
                    ('value'): timer.getTimeDuration().getRawTextContent()]
        } else {
            throw new IOException('Timer definition missing; Timer definition is required on all timers')
        }
    }
}

Edit: changed the returned array to a returned map, so you get something like: [StartEvent_0ii048j:Mon Jan 01 00:00:00 EST 2018] for easier placement of reporting data in a report mapped to the specific activityId.

In a Unit test you can then do something like:

class MyTimerTestSpec extends Specification{

  @Shared BpmnModelInstance model

  def setupSpec(){
    String path = 'bpmn/qa-test.bpmn'
    InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
    model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnTimers)
  }

   def 'test timers'(){
    when: 'loaded a process'
    then:
     println  model.evaluateTimers()

  }

}

This will eval each of the timers defined in your BPMN and ensure proper configuration. For each timer it will detect the type of timer and eval that configuration against the Camunda Business Calendar engine to generate the next Date.

You would then use the engine’s Time to make assertions about .now() and the next date.

Example: you could set the time to: Wed Jun 27 1:52pm and have a Start Event Cycle Timer that is configured with: 0 0/5 * * * ? and the result would be a Date of [Wed Jun 27 13:55:00 EDT 2018]
https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events/#time-cycle

Enjoy!

x-ref: Testing timer events in processes


Email Reminders using two boundary event timers
Any way to execute a timer job in Cockpit?
#2

Here is a update with Custom datatime control, allowing you to set future start dates so you can test the timer’s cycle and abilities:

trait bpmnTimers{
    // reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
    // https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events

    Collection<TimerEventDefinition> getTimers(){
        BpmnModelInstance model = (BpmnModelInstance)this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        return timerEventDefinitions
    }

    Map<String, Date> evaluateTimers(Date customCurrentTime = null) {
        BpmnModelInstance model = (BpmnModelInstance) this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        Map<String, Date> timers = [:]
        println customCurrentTime
        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }
        println ClockUtil.getCurrentTime()

        timerEventDefinitions.each { timer ->
            Map<String, Date> timerEval = evaluateTimer(timer)
            timers.putAll(timerEval)
        }

        if (customCurrentTime) {
            resetCurrentTime()
        }

        return timers
    }

    private void setCurrentTime(Date customCurrentTime){
        ClockUtil.setCurrentTime(customCurrentTime)
    }
    private void resetCurrentTime(){
        ClockUtil.reset()
    }

    Map<String, Date> evaluateTimer(TimerEventDefinition timer, Date customCurrentTime = null){
        Map<String,String> timerInfo = getTimerValue(timer)
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }

        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }

        switch (timerInfo) {
            case { it.type == 'date'}:
                DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
                Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : dueDate]
            case {it.type == 'cycle'}:
                CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
                Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : cycleDueDate]
            case {it.type == 'duration'}:
                DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
                Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : durationDueDate]
            default:
                throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
        }
    }

    Map<String, String> getTimerValue(TimerEventDefinition timer) {
        if (timer.getTimeDate() != null) {
            return [('type'):'date',
                    ('value'): timer.getTimeDate().getRawTextContent()]
        } else if (timer.getTimeCycle() != null) {
            return [('type'):'cycle',
                    ('value'): timer.getTimeCycle().getRawTextContent()]
        } else if (timer.getTimeDuration() != null) {
            return [('type'):'duration',
                    ('value'): timer.getTimeDuration().getRawTextContent()]
        } else {
            throw new IOException('Timer definition missing; Timer definition is required on all timers')
        }
    }
}

and you would activate this with:

...
  def 'test timers'(){
    when: 'loaded a process'
    then:
      SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy MM dd - HH:mm")
      Date customTime = simpleDateFormat.parse("2011 03 11 - 17:23")
      println  model.evaluateTimers(customTime)
  }
...

You could also test a single timer with a custom date using bpmnTimers#evaluateTimer() method.

You could also use Spock Data Tables to test a series of Start Dates to ensure that the timer will execute on the expected schedule. This is valuable for Cycle and CycleCron timers where you want to check if it is following the proper cycle.


#3

Here is a update that provide the use of Data Tables showing how to manage multiple testing periods:

class TimerTestSpec extends Specification implements, bpmnTimers{

  @Shared BpmnModelInstance model
  @Shared SimpleDateFormat dateF = new SimpleDateFormat("yyyy MM dd - HH:mm")

  def setupSpec(){
    String path = 'bpmn/qa-test.bpmn'
    InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
    model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnTimers)
  }

    def 'Start Event Cycle Cron Test'(Date customStartTime, String expectedResultTime){
    when:'Given a Timer Start Event that we eval with a custom start date of #customStartTime'
      String activityId = 'StartEvent_0ii048j'
      TimerEventDefinition timerEvent = model.getTimerById(activityId)
      Map<String,Date> result = evaluateTimer(timerEvent, customStartTime)

    then:'The timer should have a due date set to #execptedResultTime'
      assert result.get(activityId).toString() == expectedResultTime

    where:
    customStartTime                   || expectedResultTime
    dateF.parse('2011 01 01 - 00:00') || 'Sat Jan 01 01:00:00 EST 2011'
    dateF.parse('2011 01 01 - 04:49') || 'Sat Jan 01 05:00:00 EST 2011'
    dateF.parse('2011 01 01 - 05:01') || 'Sat Jan 01 10:00:00 EST 2011'
    dateF.parse('2011 01 01 - 09:59') || 'Sat Jan 01 10:00:00 EST 2011'
    dateF.parse('2011 01 01 - 14:30') || 'Sat Jan 01 15:00:00 EST 2011'
    dateF.parse('2011 01 01 - 19:30') || 'Sat Jan 01 20:00:00 EST 2011'
    dateF.parse('2011 01 01 - 21:00') || 'Tue Feb 01 01:00:00 EST 2011'
  }

}

where we have our Trait of:

trait bpmnTimers{
    // reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
    // https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events

    Collection<TimerEventDefinition> getTimers(){
        BpmnModelInstance model = (BpmnModelInstance)this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        return timerEventDefinitions
    }
    TimerEventDefinition getTimerById(String activityId){
        BpmnModelInstance model = (BpmnModelInstance)this
        TimerEventDefinition timerEventDefinition = model.getModelElementsByType(TimerEventDefinition.class).find {
                                                                            it.getParentElement().getAttributeValue('id') == activityId
                                                                        }
        return timerEventDefinition
    }

    Map<String, Date> evaluateTimers(Date customCurrentTime = null) {
        BpmnModelInstance model = (BpmnModelInstance) this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        Map<String, Date> timers = [:]
        println customCurrentTime
        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }
        println ClockUtil.getCurrentTime()

        timerEventDefinitions.each { timer ->
            Map<String, Date> timerEval = evaluateTimer(timer)
            timers.putAll(timerEval)
        }

        if (customCurrentTime) {
            resetCurrentTime()
        }

        return timers
    }

    private void setCurrentTime(Date customCurrentTime){
        ClockUtil.setCurrentTime(customCurrentTime)
    }
    private void resetCurrentTime(){
        ClockUtil.reset()
    }

    Map<String, Date> evaluateTimer(TimerEventDefinition timer, Date customCurrentTime = null){
        Map<String,String> timerInfo = getTimerValue(timer)
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }

        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }

        switch (timerInfo) {
            case { it.type == 'date'}:
                DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
                Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : dueDate]
            case {it.type == 'cycle'}:
                CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
                Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : cycleDueDate]
            case {it.type == 'duration'}:
                DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
                Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : durationDueDate]
            default:
                throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
        }
    }

    Map<String, String> getTimerValue(TimerEventDefinition timer) {
        if (timer.getTimeDate() != null) {
            return [('type'):'date',
                    ('value'): timer.getTimeDate().getRawTextContent()]
        } else if (timer.getTimeCycle() != null) {
            return [('type'):'cycle',
                    ('value'): timer.getTimeCycle().getRawTextContent()]
        } else if (timer.getTimeDuration() != null) {
            return [('type'):'duration',
                    ('value'): timer.getTimeDuration().getRawTextContent()]
        } else {
            throw new IOException('Timer definition missing; Timer definition is required on all timers')
        }
    }
}

So imagine we have a Timer Start Event with a Cycle Definition of: 0 0 1,5,10,15,20 1 * ? *. This cron basically says: On the First of Every Month, at 01:00, 05:00, 10:00, 15:00, and 20:00, the cron should execute.

So in the where statement we test this using a “customStartTime” and a “expectedResultTime” data table. I have used two different styles of data comparison for demonstration purposes for ideas on how data could be entered (as valid dates, as strings, short dates and times, etc).

So the first input start time is 2011/01/91 at 00:00 and so based on the cron the first result should be a 1am Timer.
Yada yada yada, and we arrive at the end of the data table were we have tested all of the previous scenarios, and now we are testing the change over to the next month with a start date of 2011/01/01 at 21:00, and thus past the last job which would have been at 20:00, and so the expected result time is Feb 1 at 01:00.

All assets successfully and we did not need to boot up the Camunda engine to test it out

When comparing as Strings the Errors will look like:

result.get(activityId).toString() == expectedResultTime
|      |   |           |          |  |
|      |   |           |          |  Sat Jan 01 01:10:00 EST 2011
|      |   |           |          false
|      |   |           |          1 difference (96% similarity)
|      |   |           |          Sat Jan 01 01:(0)0:00 EST 2011
|      |   |           |          Sat Jan 01 01:(1)0:00 EST 2011
|      |   |           Sat Jan 01 01:00:00 EST 2011
|      |   StartEvent_0ii048j
|      Sat Jan 01 01:00:00 EST 2011
[StartEvent_0ii048j:Sat Jan 01 01:00:00 EST 2011]


#4

@felix-mueller @Niall might be of interest for your work! ^


#5

Here is another update for working with Timer Occurrence counts:

Basically you provide a specific timer definition and a “count” which is the number of times the timer should be calculated forward in time. You can then compare this result against your predefined list:

Example:

class TimerTestSpec extends Specification implements bpmnTimers{

  @Shared BpmnModelInstance model
  @Shared SimpleDateFormat dateF = new SimpleDateFormat("yyyy MM dd - HH:mm")
  @Shared SimpleDateFormat dateBiz = new SimpleDateFormat('E MMM FF HH:mm:ss zzz yyyy')

  def setupSpec(){
    String path = 'bpmn/qa-test.bpmn'
    InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
    model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnTimers)
  }

  def 'Start Event Cycle Cron Test - Timer Occurrences'(){
    when:'Given a Timer Start Event that we get the 10 sequential occurrences'
      String activityId = 'StartEvent_0ii048j'
      TimerEventDefinition timerEvent = model.getTimerById(activityId)
      Date customStartTime = dateF.parse('2011 01 01 - 00:00')
      List<Date> timerOccurrences = getTimerOccurencesByCount(timerEvent, 10, customStartTime)

    and: 'a expected set of dates are established'
      List<Date> expectedTimerOccurrences = ['Sat Jan 01 01:00:00 EST 2011',
                                            'Sat Jan 01 05:00:00 EST 2011',
                                            'Sat Jan 01 10:00:00 EST 2011',
                                            'Sat Jan 01 15:00:00 EST 2011',
                                            'Sat Jan 01 20:00:00 EST 2011',
                                            'Tue Feb 01 01:00:00 EST 2011',
                                            'Tue Feb 01 05:00:00 EST 2011',
                                            'Tue Feb 01 10:00:00 EST 2011',
                                            'Tue Feb 01 15:00:00 EST 2011',
                                            'Tue Feb 01 20:00:00 EST 2011'].collect {dateBiz.parse(it)}

    then:'The timer should generate timer events for the following 10 dates'
      assert timerOccurrences == expectedTimerOccurrences

        // Use string comparison to show more detailed error to more easily
        // identify where the error was in the list
//      assert timerOccurrences.toString() == expectedTimerOccurrences.toString()
  }

}

and the updated traits are:

trait bpmnTimers{
    // reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
    // https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events

    Collection<TimerEventDefinition> getTimers(){
        BpmnModelInstance model = (BpmnModelInstance)this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        return timerEventDefinitions
    }
    TimerEventDefinition getTimerById(String activityId){
        BpmnModelInstance model = (BpmnModelInstance)this
        TimerEventDefinition timerEventDefinition = model.getModelElementsByType(TimerEventDefinition.class).find {
            it.getParentElement().getAttributeValue('id') == activityId
        }
        return timerEventDefinition
    }

    Map<String, Date> evaluateTimers(Date customCurrentTime = null) {
        BpmnModelInstance model = (BpmnModelInstance) this
        Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
        Map<String, Date> timers = [:]
        println customCurrentTime
        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }
        println ClockUtil.getCurrentTime()

        timerEventDefinitions.each { timer ->
            Map<String, Date> timerEval = evaluateTimer(timer)
            timers.putAll(timerEval)
        }

        if (customCurrentTime) {
            resetCurrentTime()
        }

        return timers
    }

    private void setCurrentTime(Date customCurrentTime){
        ClockUtil.setCurrentTime(customCurrentTime)
    }
    private void resetCurrentTime(){
        ClockUtil.reset()
    }

    List<Date> getTimerOccurencesByCount(TimerEventDefinition timer, Integer count, Date customCurrentTime = null){
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }
        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }

        List<Date> timerOccurrences = new ArrayList<Date>()
        Date timerEvalDate = customCurrentTime

        1.upto(count, {
            Date evalResult = evaluateTimer(timer, timerEvalDate).get(activityId)
            timerEvalDate = evalResult
            timerOccurrences << evalResult
        })
        return timerOccurrences
    }

    Map<String, Date> evaluateTimer(TimerEventDefinition timer, Date customCurrentTime = null){
        Map<String,String> timerInfo = getTimerValue(timer)
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }

        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }

        switch (timerInfo) {
            case { it.type == 'date'}:
                DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
                Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : dueDate]
            case {it.type == 'cycle'}:
                CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
                Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : cycleDueDate]
            case {it.type == 'duration'}:
                DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
                Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)

                if (customCurrentTime){
                    resetCurrentTime()
                }

                return [(activityId) : durationDueDate]
            default:
                throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
        }
    }

    Map<String, String> getTimerValue(TimerEventDefinition timer) {
        if (timer.getTimeDate() != null) {
            return [('type'):'date',
                    ('value'): timer.getTimeDate().getRawTextContent()]
        } else if (timer.getTimeCycle() != null) {
            return [('type'):'cycle',
                    ('value'): timer.getTimeCycle().getRawTextContent()]
        } else if (timer.getTimeDuration() != null) {
            return [('type'):'duration',
                    ('value'): timer.getTimeDuration().getRawTextContent()]
        } else {
            throw new IOException('Timer definition missing; Timer definition is required on all timers')
        }
    }
}

specifically the following method:

...
List<Date> getTimerOccurencesByCount(TimerEventDefinition timer, Integer count, Date customCurrentTime = null){
        String activityId = timer.getParentElement().getAttributeValue('id')
        if (activityId == null){
            throw new IOException('Could not get Activity Id of Timer Event Definition')
        }
        if (customCurrentTime){
            setCurrentTime(customCurrentTime)
        }

        List<Date> timerOccurrences = new ArrayList<Date>()
        Date timerEvalDate = customCurrentTime

        1.upto(count, {
            Date evalResult = evaluateTimer(timer, timerEvalDate).get(activityId)
            timerEvalDate = evalResult
            timerOccurrences << evalResult
        })
        return timerOccurrences
    }
...

This lets us get a list of the timers occurrences that would match execution, and this easily test a expected list of sequences occurrence of timer executions against the list.

So our Unit test has the important parts of:

...
List<Date> timerOccurrences = getTimerOccurencesByCount(timerEvent, 10, customStartTime)
...

Where we run the timerEvent by 10 occurrence using our customStartTime. Each occurrence of the timer will move time forward to the moment of the previous timer.

We can then set the expected values:

...
    and: 'a expected set of dates are established'
      List<Date> expectedTimerOccurrences = ['Sat Jan 01 01:00:00 EST 2011',
                                            'Sat Jan 01 05:00:00 EST 2011',
                                            'Sat Jan 01 10:00:00 EST 2011',
                                            'Sat Jan 01 15:00:00 EST 2011',
                                            'Sat Jan 01 20:00:00 EST 2011',
                                            'Tue Feb 01 01:00:00 EST 2011',
                                            'Tue Feb 01 05:00:00 EST 2011',
                                            'Tue Feb 01 10:00:00 EST 2011',
                                            'Tue Feb 01 15:00:00 EST 2011',
                                            'Tue Feb 01 20:00:00 EST 2011'].collect {dateBiz.parse(it)}

    then:'The timer should generate timer events for the following 10 dates'
      assert timerOccurrences == expectedTimerOccurrences
...

and assert that the actual occurrences and the expected occurrences match.

No need to run execution of the BPMN model! :tada::t_rex:

Enjoy!

and note: for the business analysts that might write some of these tests, you can further simpify the unit test by removing the “typing” and just use things like def:

...
 and: 'a expected set of dates are established'
      def expectedTimerOccurrences = ['Sat Jan 01 01:00:00 EST 2011',
                                            'Sat Jan 01 05:00:00 EST 2011',
                                            'Sat Jan 01 10:00:00 EST 2011',
                                            'Sat Jan 01 15:00:00 EST 2011',
                                            'Sat Jan 01 20:00:00 EST 2011',
                                            'Tue Feb 01 01:00:00 EST 2011',
                                            'Tue Feb 01 05:00:00 EST 2011',
                                            'Tue Feb 01 10:00:00 EST 2011',
                                            'Tue Feb 01 15:00:00 EST 2011',
                                            'Tue Feb 01 20:00:00 EST 2011'].collect {dateBiz.parse(it)}

    then:'The timer should generate timer events for the following 10 dates'
      assert timerOccurrences == expectedTimerOccurrences
...

No need to worry about getting the typing correct

Edit:
For those wondering you get error output such as:

timerOccurrences == expectedTimerOccurrences
|                |  |
|                |  [Sat Jan 01 01:10:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]
|                false
[Sat Jan 01 01:00:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]

Or if you compare as strings, you can more robust information:

timerOccurrences.toString() == expectedTimerOccurrences.toString()
|                |          |  |                        |
|                |          |  |                        [Sat Jan 01 01:10:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]
|                |          |  [Sat Jan 01 01:10:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]
|                |          false
|                |          1 difference (95% similarity) (comparing subset start: 4, end1: 26, end2: 26)
|                |           Jan 01 01:(0)0:00 EST 2
|                |           Jan 01 01:(1)0:00 EST 2
|                [Sat Jan 01 01:00:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]
[Sat Jan 01 01:00:00 EST 2011, Sat Jan 01 05:00:00 EST 2011, Sat Jan 01 10:00:00 EST 2011, Sat Jan 01 15:00:00 EST 2011, Sat Jan 01 20:00:00 EST 2011, Tue Feb 01 01:00:00 EST 2011, Tue Feb 01 05:00:00 EST 2011, Tue Feb 01 10:00:00 EST 2011, Tue Feb 01 15:00:00 EST 2011, Tue Feb 01 20:00:00 EST 2011]

For longer lists of dates, the use of strings will be beneficial because you will be able to easily identify exactly where the issue is. :+1: