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