Consistent testing with methods that depend on Date.today


#1

Hi everyone

I’m building an app in which quite a bit of data is dependant on dates
(and times), and I have logic that depends on when ‘today’ is. I’m
trying to get some better functional tests written, and am unsure of
the best way to approach it.

The problem is that I want the tests to run against the same data
every time, whether I run it on Tuesday or Thursday or whatever day.
My fixtures are dynamic in that they add data relative to the
beginning of ‘this week’ (day of the week is important for this data
to make sense, so I can’t go relative to Date.today directly). In one
of my actions, I return data for the ‘next three days’, and as such,
tests are run against different data depending on what day of the week
the tests are run on. This is not what I want - tests should get run
against the same data each time (imho).

I know this problem has to have been solved in the past, but I just
can’t seem to put my finger on a clean method to run these tests on a
consistent set of data. Do people mock out the Date.today method to
return a specific date, or what?

Thanks for any hints,
Dave


Dave Goodlad
removed_email_address@domain.invalid or removed_email_address@domain.invalid
http://david.goodlad.ca/


#2

David G. wrote:

I know this problem has to have been solved in the past, but I just
can’t seem to put my finger on a clean method to run these tests on a
consistent set of data. Do people mock out the Date.today method to
return a specific date, or what?
I can’t say I’ve had exactly this problem, but that’s the first thing
I’d try. Either that or extract out whatever date-retrieval
functionality you need to a separate model class which uses Date.* in
the actual app, but static data in a mock.


#3

Alex Y. wrote:

David G. wrote:

I know this problem has to have been solved in the past, but I just
can’t seem to put my finger on a clean method to run these tests on a
consistent set of data. Do people mock out the Date.today method to
return a specific date, or what?
I can’t say I’ve had exactly this problem, but that’s the first thing
I’d try. Either that or extract out whatever date-retrieval
functionality you need to a separate model class which uses Date.* in
the actual app, but static data in a mock.

I’m sure you can apply my approach the following approach to fit your
problem.

I’ve a file mutable_time.rb which I include when running my tests.

class << Time
alias_method :original_now, :now

def set(time)
@now = time
end

def advance_now_in_seconds(seconds)
@now ||= now
@now += seconds
end

def now
@now || original_now
end

def use_original_now
@now = nil
end
end

I don’t use dynamic fixtures - I find it easier to keep the fixtures the
same - and test different scenarios by changing Time.now - that way it’s
easier to talk about fixtures when discussing acceptance tests etc.

so if i wanted to test what happens if time is same as the time in my
fixture then I’d call something like

Time.set(fixture(:item).date)

Hope that helps/


#4

On 1/18/06, Darragh C. removed_email_address@domain.invalid wrote:

I’m sure you can apply my approach the following approach to fit your
problem.

I’ve a file mutable_time.rb which I include when running my tests.

class << Time
[snip]

I don’t use dynamic fixtures - I find it easier to keep the fixtures the
same - and test different scenarios by changing Time.now - that way it’s
easier to talk about fixtures when discussing acceptance tests etc.

so if i wanted to test what happens if time is same as the time in my
fixture then I’d call something like

Time.set(fixture(:item).date)

Do you have any problems with any of Rails’ built-in functionality
getting confused when you redefine Time#now like that? After some
experimentation, it seems that the Date class relies on Time#now, so I
could isolate all of my “meddling” to changing one definition and have
it remain consistent throughout the app. I’m just concerned that I’ll
break something subtle in the process!

Dave

Hope that helps/


Posted via http://www.ruby-forum.com/.


Rails mailing list
removed_email_address@domain.invalid
http://lists.rubyonrails.org/mailman/listinfo/rails


Dave Goodlad
removed_email_address@domain.invalid or removed_email_address@domain.invalid
http://david.goodlad.ca/


#5

David G. wrote:

Do you have any problems with any of Rails’ built-in functionality
getting confused when you redefine Time#now like that?

I typically have a teardown to reset to normal time -

def teardown
Time.use_system_time
end

only weirdness I’ve seen is the time report when you run a test can
start saying billions of seconds - but i think that’s only when i forget
to have a teardown.


#6

I’ve had good luck isolating the date-sensitive code and making ‘now’
a parameter. You can also parameter-ize time-outs and such, and set
them to short time intervals. Of course, these approaches won’t work
for some scenarios. It’s one of the toughest test nuts to crack.

There was a good discussion of alternatives in one of the XP books, I
think, but just can’t find it.

Scott


#7

I had more or less the same problem, and I solved it by overriding
Time::now

My app takes timezones and daylight savings time into account, so I
wanted
to be able to set “today” to days that are or are not in DST.

I’m also using Salted Login Generator, which does it’s own (different)
override of Time::now, so I wrote some code that incorporates both
pieces of
logic.

Following is my time.rb, which I use only for testing. If you don’t
care
about the Salted Login Generator stuff, you can ignore
“@@advance_by_days
related stuff.

To use it, you can call “Time.set_now_gmt(year, month, day, hour,
minute,
second)”, and then call “Time.clear_now_gmt”. Better, you can pass a
block
into set_now_gmt, and the change will only affect that block of code.

require ‘time’

class Time
@@advance_by_days = 0
cattr_accessor :advance_by_days

#MES- This is some super-funky stuff to test times and timezones. To

properly test timezones, we need to test at times that are affected

by

daylight savings time (e.g. to see that DST doesn’t screw up our

local to UTC conversions, etc.) But we can’t count on tests being

run

on days that are near a DST switchover, so we need to fake out the

test-

to pretend that we’re running at one of those times. These

functions

let the caller set what is considered “now” by the Time class, which

cascades

to other Time functions.

@@now_arr = nil

def self.set_now_gmt(y, m = 1, d = 1, h = 0, mi = 0, s = 0)
@@now_arr = [y, m, d, h, mi, s]
if block_given?
begin
yield
ensure
clear_now_gmt
end
end
end

def self.clear_now_gmt
@@now_arr = nil
end

def self.set_advance_by_days(days)
@@advance_by_days = days
if block_given?
begin
yield
ensure
@@advance_by_days = 0
end
end
end

#MES- OK, this is kinda complicated. I wrote code to set what’s
considered

‘now’ (see the comment above.) However, the Salted Login test code

modifies

‘now’ in a different manner- they use an offset, whereas my code

sets an

absolute time. I’ve merged these approaches into the ‘now’ method.

If
there’s

an offset, it’s used. If there’s no offset, but there’s an absolute

date, it’s used.

If neither tweak is set up, the current time is used.

def self.now
if Time.advance_by_days != 0
return Time.at(new.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
elsif !@@now_arr.nil?
self.utc @@now_arr[0], @@now_arr[1], @@now_arr[2], @@now_arr[3],
@@now_arr[4], @@now_arr[5]
else
new
end
end

end