Testing Eventmachine periodic timers

Published by Lorenzo Planas on September 22, 2013

EventMachine is a great option to run periodic timers that require more flexibility or maintainability than just using cron.

However, testing the correctness of that timer logic may be tricky, due to EventMachine's reactor pattern.

When testing the timers we can focus on:

  1. Ensuring the logic associated to the timer is run.
  2. Implementing a way to stop the reactor so the test ends in a finite (and short) time.

The example below tries to illustrate this. Point 1 is implemented with mocking, since the focus here is to check the logic is executed, not on the details of what is executed, which probably belong in separate objects and associated specs. Point 2 is implemented by passing an optional block, where we stop the reactor once the test has exercised the code.

require 'eventmachine'

module Sample
  class Runner
    DEFAULT_INTERVAL_IN_SECS = 1

    def initialize(command, interval=DEFAULT_INTERVAL_IN_SECS)
      @command  = command
      @interval = interval
    end

    def run(&after_command)
      EventMachine.run {
        EventMachine::PeriodicTimer.new(interval) {
          command.run
          after_command.call if after_command
        }
      }
    end

    private

    attr_reader :command, :interval
  end # Runner
end # Sample
gem 'minitest'
require 'minitest/autorun'
require_relative './runner'

describe Sample::Runner do
  describe '#run' do
    it 'runs a command' do
      command = Minitest::Mock.new
      runner  = Sample::Runner.new(command)

      command.expect :run, self

      runner.run { EventMachine.stop }
      command.verify
    end

    it 'runs the loop at the interval set at initialization' do
      interval_in_secs  = 0.5
      runner            = Sample::Runner.new(fake_command, interval_in_secs)

      before  = Time.now.to_i
      runner.run { EventMachine.stop }
      after   = Time.now.to_i

      (before - after < 1).must_equal true
    end

    it 'executes a block after running the command' do
      runner        = Sample::Runner.new(fake_command)
      timer_loops   = 0

      return_value  = runner.run { 
        timer_loops = timer_loops + 1
        EventMachine.stop if timer_loops > 1
      }

      timer_loops.must_equal 2
    end
  end

  def fake_command
    command = Object.new
    def command.run; self; end
    command
  end 
end # Sample::Runner