What's the best way to set Time.now for the purpose of testing time-sensitive methods in a unit test?
I really like the Timecop library. You can do time warps in block form (just like time-warp):
Timecop.travel(6.days.ago) do
#model = TimeSensitiveMode.new
end
assert #model.times_up!
(Yes, you can nest block-form time travel.)
You can also do declarative time travel:
class MyTest < Test::Unit::TestCase
def setup
Timecop.travel(...)
end
def teardown
Timecop.return
end
end
I have some cucumber helpers for Timecop here. They let you do things like:
Given it is currently January 24, 2008
And I go to the new post page
And I fill in "title" with "An old post"
And I fill in "body" with "..."
And I press "Submit"
And we jump in our Delorean and return to the present
When I go to the home page
I should not see "An old post"
Personally I prefer to make the clock injectable, like so:
def hello(clock=Time)
puts "the time is now: #{clock.now}"
end
Or:
class MyClass
attr_writer :clock
def initialize
#clock = Time
end
def hello
puts "the time is now: #{#clock.now}"
end
end
However, many prefer to use a mocking/stubbing library. In RSpec/flexmock you can use:
Time.stub!(:now).and_return(Time.mktime(1970,1,1))
Or in Mocha:
Time.stubs(:now).returns(Time.mktime(1970,1,1))
I'm using RSpec and I did this: Time.stub!(:now).and_return(2.days.ago) before I call Time.now. In that way I'm able to control the time I used for that particular test case
Using Rspec 3.2, the only simple way I found to fake Time.now return value is :
now = Time.parse("1969-07-20 20:17:40")
allow(Time).to receive(:now) { now }
Now Time.now will always return the date of Apollo 11 landing on the moon.
Source: https://www.relishapp.com/rspec/rspec-mocks/docs
Do the time-warp
time-warp is a library that does what you want. It gives you a method that takes a time and a block and anything that happens in the block uses the faked time.
pretend_now_is(2000,"jan",1,0) do
Time.now
end
Don't forget that Time is merely a constant that refers to a class object. If you're willing to cause a warning, you can always do
real_time_class = Time
Time = FakeTimeClass
# run test
Time = real_time_class
If you have ActiveSupport included, you could use:
travel_to Time.zone.parse('2010-07-05 08:00')
http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html
Also see this question where I put this comment as well.
Depending upon what you are comparing Time.now to, sometimes you can change your fixtures to accomplish the same goal or test the same feature. For example, I had a situation where I needed one thing to happen if some date was in the future and another to happen if it was in the past. What I was able to do was include in my fixtures some embedded ruby (erb):
future:
comparing_date: <%= Time.now + 10.years %>
...
past:
comparing_date: <%= Time.now - 10.years %>
...
Then in your tests then you choose which one to use to test the different features or actions based upon the time relative to Time.now.
Had the same issue, I had to fake time for a spec for a specific day and time just did that:
Time.stub!(:now).and_return(Time.mktime(2014,10,22,5,35,28))
this will give you:
2014-10-22 05:35:28 -0700
This kind of works and allows for nesting:
class Time
class << self
attr_accessor :stack, :depth
end
def self.warp(time)
Time.stack ||= []
Time.depth ||= -1
Time.depth += 1
Time.stack.push time
if Time.depth == 0
class << self
alias_method :real_now, :now
alias_method :real_new, :new
define_method :now do
stack[depth]
end
define_method :new do
now
end
end
end
yield
Time.depth -= 1
Time.stack.pop
class << self
if Time.depth < 0
alias_method :new, :real_new
alias_method :now, :real_now
remove_method :real_new
remove_method :real_now
end
end
end
end
It could be slightly improved by undefing the stack and depth accessors at the end
Usage:
time1 = 2.days.ago
time2 = 5.months.ago
Time.warp(time1) do
Time.real_now.should_not == Time.now
Time.now.should == time1
Time.warp(time2) do
Time.now.should == time2
end
Time.now.should == time1
end
Time.now.should_not == time1
Time.now.should_not be_nil
Depending upon what you are comparing Time.now to, sometimes you can change your fixtures to accomplish the same goal or test the same feature. For example, I had a situation where I needed one thing to happen if some date was in the future and another to happen if it was in the past. What I was able to do was include in my fixtures some embedded ruby (erb):
future:
comparing_date: <%= Time.now + 10.years %>
...
past:
comparing_date: <%= Time.now - 10.years %>
...
Then in your tests then you choose which one to use to test the different features or actions based upon the time relative to Time.now.
i just have this in my test file:
def time_right_now
current_time = Time.parse("07/09/10 14:20")
current_time = convert_time_to_utc(current_date)
return current_time
end
and in my Time_helper.rb file i have a
def time_right_now
current_time= Time.new
return current_time
end
so when testing the time_right_now is overwritten to use what ever time you want it to be.
I allways extract Time.now into a separate method that I turn into attr_accessor in the mock.
The recently-released Test::Redef makes this and other fakery easy, even without restructuring the code in a dependency-injection style (especially helpful if you're using other peoples' code.)
fake_time = Time.at(12345) # ~3:30pm UTC Jan 1 1970
Test::Redef.rd 'Time.now' => proc { fake_time } do
assert_equal 12345, Time.now.to_i
end
However, be careful of other ways to obtain time that this will not fake out (Date.new, a compiled extension that makes its own system call, interfaces to things like external database servers which know current timestamps, etc.) It sounds like the Timecop library above might overcome these limitations.
Other great uses include testing things like "what happens when I'm trying to use this friendly http client but it decides to raise this an exception instead of returning me a string?" without actually setting up the network conditions which lead to that exception (which may be tricky). It also lets you check the arguments to redef'd functions.
My own solution https://github.com/igorkasyanchuk/rails_time_travel - a gem with UI, so you don't need to hardcode any datetime in the code. Just change it from the UI.
It might be also very useful for you QA's team, or testing app on the staging.
Related
I have a large amount of Minitest unit tests (methods), over 300. They all take some time, from a few milliseconds to a few seconds. Some of them hang up, sporadically. I can't understand which one and when.
I want to apply Timeout to each of them, to make sure anyone fails if it takes longer than, say, 5 seconds. Is it achievable?
For example:
class FooTest < Minitest::Test
def test_calculates_something
# Something potentially too slow
end
end
You can use the Minitest PLugin loader to load a plugin. This is, by far, the cleanest solution. The plugin system is not very well documented, though.
Luckily, Adam Sanderson wrote an article on the plugin system.
The best news is that this article explains the plugin system by writing a sample plugin that reports slow tests. Try out minitest-snail, it is probably almost what you want.
With a little modification we can use the Reporter to mark a test as failed if it is too slow, like so (untested):
File minitest/snail_reporter.rb:
module Minitest
class SnailReporter < Reporter
attr_reader :max_duration
def self.options
#default_options ||= {
:max_duration => 2
}
end
def self.enable!(options = {})
#enabled = true
self.options.merge!(options)
end
def self.enabled?
#enabled ||= false
end
def initialize(io = STDOUT, options = self.class.options)
super
#max_duration = options.fetch(:max_duration)
end
def record result
#passed = result.time < max_duration
slow_tests << result if !#passed
end
def passed?
#passed
end
def report
return if slow_tests.empty?
slow_tests.sort_by!{|r| -r.time}
io.puts
io.puts "#{slow_tests.length} slow tests."
slow_tests.each_with_index do |result, i|
io.puts "%3d) %s: %.2f s" % [i+1, result.location, result.time]
end
end
end
end
File minitest/snail_plugin.rb:
require_relative './snail_reporter'
module Minitest
def self.plugin_snail_options(opts, options)
opts.on "--max-duration TIME", "Report tests that take longer than TIME seconds." do |max_duration|
SnailReporter.enable! :max_duration => max_duration.to_f
end
end
def self.plugin_snail_init(options)
if SnailReporter.enabled?
io = options[:io]
Minitest.reporter.reporters << SnailReporter.new(io)
end
end
end
This simple method on a class just run the status method using the safe navigation operator.
def current_status
account&.status
end
But reek report this warning:
MyClass#current_status performs a nil-check [https://github.com/troessner/reek/blob/master/docs/Nil-Check.md]
How can I properly write methods like this to avoid Nil Check?
I've also verified this post from thoughtbot but it seem like "too much" for just a safe navigation operator.
Ruby 2.3.1
The advice from "Example 4" in the linked post is verbose but pretty good :
class MyClass
def initialize(with_account = nil)
#account = Account.new if with_account
end
def current_status
account.status
end
def account
#account || NilAccount.new
end
end
class Account
def status
"Up!"
end
end
class NilAccount
def status
"Down!"
end
end
puts MyClass.new(:with_account).current_status
#=> "Up!"
puts MyClass.new.current_status
#=> "Down!"
If it's "too much" for you, account&.status might be just fine.
Whatever you do : you'll need to test your code as much as possible!
well, tell-dont-ask looks pretty good, but Example 4 looks like an overkill to resolve this specific case.
#andredurao I think, we can use this workaround to pass checks, for some reason reek is fine with it:
def current_status
return unless account
account.status
end
I have a class like this.
class Time
def has_same_hours?(t)
self.strftime("%Y%m%d%H") == t.strftime("%Y%m%d%H")
end
end
class MyLogger
DATA_DIR = 'data'
def initialize
#time_current_hour = Time.now
#io = nil
update_io_to_current_hour
end
def update_io_to_current_hour
#io = open output_filename, "a+" if #io.nil?
return if #time_current_hour.has_same_hours? Time.now
#io.close
#io = open output_filename, "a+"
#time_current_hour = Time.now
end
def output_filename(time = Time.now)
"#{DATA_DIR}/#{time.strftime('%Y_%m_%d_%H')}.txt"
end
end
When update_io_to_current_hour is called, the file IO should be changed if hour is different compare to #time_current_hour.
I want to write RSpec test for it. This is what I wrote.
describe Logger do
let(:logger){ Logger.new }
describe "#update_io_to_current_hour" do
context "when the hour changes" do
before{
#time_now = Time.parse("2010/4/10 19:00")
#time_current = Time.parse("2010/4/10 18:59")
Time.stub(:now).and_return(#time_now)
logger.stub(:time_current_hour).and_return(#time_current)
}
it "should change file io" do
expect{logger.update_io_to_current_hour}.to change{ logger.instance_variable_get :#io }
end
end
context "when the hour doesn't changes" do
before{
#time_now = Time.parse("2010/4/10 18:59")
#time_current = Time.parse("2010/4/10 18:58")
Time.stub(:now).and_return(#time_now)
logger.stub(:time_current_hour).and_return(#time_current)
}
it "should not change file io" do
expect{logger.update_io_to_current_hour}.not_to change{ logger.instance_variable_get :#io }
end
end
end
end
Second test passes and first not. It looks like file io is never changed whatever stubbed to Time object.
What am I doing wrong? How can I write the test properly?
A couple of points:
logger.stub(:time_current_hour)
The class has no method named :time_current_hour, only an instance variable. There is rarely a good reason to test the values of instance variables; that is an implementation detail. You want to test behavior. In any case this stub is ineffective. Also
logger.instance_variable_get :#io
Now you are reaching right into the guts of your object and inspecting its internal values. Have you no regard for its privacy? :)
I think this would be a lot easier if you simply tested the value of :output_filename. When the hour changes, the filename changes. When the hour is the same, the filename is the same.
I've extracted a single class from a Rails app into a gem. It's very, very simple, but of course I'd like to fully test it (I'm using rspec).
The class does some simple date-calculation. It's not dependent on Rails, but since it started out in a Rails app, and is still used there, it uses ActiveSupport's time zone-aware methods when it can. But, if ActiveSupport isn't available, it should use the std-lib Date methods.
Specifically, it only does this in one single place: Defaulting an optional argument to "today's date":
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
Question is: How do I properly test this? If I require ActiveSupport in my spec_helper.rb, it'll obviously always use that. If I don't require it anywhere, it'll never use it. And if I require it for a single example group, rspec's random execution order makes the testing unpredictable, as I don't know when AS will be required.
I can require maybe it in a before(:all) in a nested group, as nested groups are (I believe) processed highest to deepest. But that seems terribly inelegant.
I could also split the specs into two files, and run them separately, but again, that seems unnecessary.
I could also disable rspec's random ordering, but that's sort of going against the grain. I'd rather have it as randomized as possible.
Any ideas?
Another solution is to mock the current and today methods, and use those for testing. Eg:
# you won't need these two lines, just there to make script work standalone
require 'rspec'
require 'rspec/mocks/standalone'
def test_method(arg = nil)
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
arg
end
describe "test_method" do
let(:test_date) { Date.new(2001, 2, 3) }
it "returns arg unchanged if not nil" do
test_method(34).should == 34
end
context "without Date.current available" do
before(:all) do
Date.stub(:today) { test_date }
end
it "returns Date.today when arg isn't present" do
test_method.should == test_date
end
end
context "with Date.current available" do
before(:all) do
Date.stub(:current) { test_date }
end
it "returns Date.current when arg isn't present" do
test_method.should == test_date
end
end
end
Running with rspec test.rb results in the tests passing.
Also, the stubs are present only in each context, so it doesn't matter what order the specs are run in.
This is more than a little perverse, but it should work. Include ActiveSupport, and then:
context "without ActiveSupport's Date.current" do
before(:each) do
class Date
class << self
alias_method :current_backup, :current
undef_method :current
end
end
end
# your test
after(:each) do
class Date
class << self
alias_method :current, :current_backup
end
end
end
end
I can't really recommend this; I would prefer to split out this one spec and run it separately as you suggested.
I know. This is discouraged. For reasons I won't get into, I need to run my tests in the order they are written. According to the documentation, if my test class (we'll call it TestClass) extends Minitest::Unit::TestCase, then I should be able to call the public method i_suck_and_my_tests_are_order_dependent! (Gee - do you think the guy who created Minitest had an opinion on that one?). Additionally, there is also the option of calling a method called test_order and specifying :alpha to override the default behavior of :random. Neither of these are working for me.
Here's an example:
class TestClass < Minitest::Unit::TestCase
#override random test run ordering
i_suck_and_my_tests_are_order_dependent!
def setup
...setup code
end
def teardown
...teardown code
end
def test_1
test_1 code....
assert(stuff to assert here, etc...)
puts 'test_1'
end
def test_2
test_2_code
assert(stuff to assert here, etc...)
puts 'test_2'
end
end
When I run this, I get:
undefined method `i_suck_and_my_tests_are_order_dependent!' for TestClass:Class (NoMethodError)
If I replace the i_suck method call with a method at the top a la:
def test_order
:alpha
end
My test runs, but I can tell from the puts for each method that things are still running in random order each time I run the tests.
Does anyone know what I'm doing wrong?
Thanks.
If you just add test_order: alpha to your test class, the tests will run in order:
class TestHomePage
def self.test_order
:alpha
end
def test_a
puts "a"
end
def test_b
puts "b"
end
end
Note that, as of minitest 5.10.1, the i_suck_and_my_tests_are_order_dependent! method/directive is completely nonfunctional in test suites using MiniTest::Spec syntax. The Minitest.test_order method is apparently not being called at all.
EDIT: This has been a known issue since Minitest 5.3.4: see seattlerb/minitest#514 for the blow-by-blow wailing and preening.
You and I aren't the ones who "suck". What's needed is a BDD specification tool for Ruby without the bloat of RSpec and without the frat-boy attitude and contempt for wider community practices of MiniTest. Does anyone have any pointers?
i_suck_and_my_tests_are_order_dependent! may be a later addition to minitest & not available as a Ruby core method. In that case, you'd want to force use of your gem version:
require 'rubygems'
gem 'minitest'
I think that the method *test_order* should be a class method and not a instance method like so:
# tests are order dependent
def self.test_order
:alpha
end
The best way to interfere in this chain may be to override a class method runnable_methods:
def self.runnable_methods
['run_first'] | super | ['run_last']
end
#Minitest version:
def self.runnable_methods
methods = methods_matching(/^test_/)
case self.test_order
when :random, :parallel then
max = methods.size
methods.sort.sort_by { rand max }
when :alpha, :sorted then
methods.sort
else
raise "Unknown test_order: #{self.test_order.inspect}"
end
end
You can reorder test any suitable way around. If you define your special ordered tests with
test 'some special ordered test' do
end
, don't forget to remove them from the results of super call.
In my example I need to be sure only in one particular test to run last, so I keep random order on whole suite and place 'run_last' at the end of it.