Current Time

Obtaining the current time with Time.now is a classic example of a side effect. Code relying on accessing system time is harder to test. One possible solution is passing time around explicitly, but using effects can save you some typing depending on the case.

Providing and obtaining the current time is straightforward:

require 'dry/effects'

class CurrentTimeMiddleware
  include Dry::Effects::Handler.CurrentTime

  def initialize(app)
    @app = app
  end

  def call(env)
    # It will use Time.now internally once and set it fixed
    with_current_time do
      @app.(env)
    end
  end
end

###

class CreateSubscription
  include Dry::Effects.Resolve(:subscription_repo)
  include Dry::Effects.CurrentTime

  def call(values)
    subscription_repo.create(
      values.merge(start_at: current_time)
    )
  end
end

Providing time in tests

A typical usage would be:

require 'dry/effects'

RSpec.configure do |config|
  config.include Dry::Effects::Handler.CurrentTime
  config.include Dry::Effects.CurrentTime

  config.around { |ex| with_current_time(&ex) }
end

Then anywhere in tests, you can use it:

it 'uses current time as a start' do
  subscription = create_subscription(...)
  expect(subscription.start_at).to eql(current_time)
end

To change the time, call with_current_time with a proc:

it 'closes a subscription with current time' do
  future = current_time + 86_400
  closed_subscription = with_current_time(proc { future }) { close_subscription(subscription) }
  expect(closed_subscription.closed_at).to eql(future)
end

Wrapping time with a proc is required, read about generators below.

Time rounding

current_time accepts an argument for rounding time values. It can be passed statically to the module builder or dynamically to the effect constructor:

class CreateSubscription
  include Dry::Effects.CurrentTime(round: 3)

  def call(...)
    # value will be rounded to milliseconds
    current_time
    # value will be rounded to microseconds
    current_time(round: 6)
  end
end

Time is fixed

By default, calling with_current_time even without arguments will freeze the current time. This means current_time will return the same value during request processing etc.

You can "unfix" time with passing fixed: false to the handler builder:

include Dry::Effects::Handler.CurrentTime(fixed: false)

However, this is not recommended because it will make the behavior of current_time different in tests (where you pass a fixed value) and in a production environment.

Using a custom generator

The default time provider accepts a custom generator which is a simple callable object. This way you can pass a proc with fixed time:

frozen = Time.now
with_fixed_time(proc { frozen }) do
  # ...
end

Or you can change time on every call:

start = Time.now
with_fixed_time(proc { start += 0.1 }) do
  # ...
end

Discrete time shifts

If you pass step: x to the handler, it will shift the current time on every access by x:

with_fixed_time(step: 0.1) do
  current_time # => ... 18:00:00.000
  current_time # => ... 18:00:00.100
  current_time # => ... 18:00:00.200
end

You can also pass initial time:

initial = Time.new(1970)
with_fixed_time(initial: initial, step: 60) do
  current_time # => 1970-01-01 00:00:00 +0000
  current_time # => 1970-01-01 00:01:00 +0000
  current_time # => 1970-01-01 00:02:00 +0000
end

Overriding handlers

Handlers of current time can be overridden by an outer handler if you pass overridable: true:

require 'dry/effects'

class CurrentTimeMiddleware
  include Dry::Effects::Handler.CurrentTime

  def initialize(app)
    @app = app
  end

  def call(env)
    with_current_time(overridable: ENV['RACK_ENV'].eql?('test')) do
      @app.(env)
    end
  end
end

It's usually done in tests:

# Using global time
frozen_time = Time.now

puts "Running with time #{frozen_time.iso8601}" if ENV['CI']

RSpec.configure do |config|
  config.include Dry::Effects::Handler.CurrentTime
  config.include(Module.new { define_method(:current_time) { frozen_time } })
  config.around { |ex| with_current_time(proc { frozen_time }, &ex) }
end

octocatEdit on GitHub