Time Travel for Data
FakeDataDSL's Time Travel feature lets you generate data as it would have existed at a specific point in time, or create realistic progressions of data over time. Perfect for testing time-sensitive features, historical reports, and temporal queries.
Quick Start
# Generate data from 30 days ago
FakeDataDSL::TimeTravel.at(30.days.ago) do
user = FakeDataDSL.generate("User")
# user[:created_at] is 30 days ago
# user[:updated_at] is 30 days ago
end
# Generate data progression
orders = FakeDataDSL::TimeTravel.progression("Order",
from: 1.year.ago,
to: Time.current,
count: 12
)
# 12 orders spread across the past year
Basic Usage
Generate at Specific Time
# All timestamps generated within the block are relative to the travel time
FakeDataDSL::TimeTravel.at(Date.new(2025, 1, 1)) do
user = FakeDataDSL.generate("User")
# user[:created_at] => 2025-01-01 (random time on that day)
end
# With specific time
FakeDataDSL::TimeTravel.at(Time.new(2025, 6, 15, 14, 30, 0)) do
event = FakeDataDSL.generate("Event")
# event[:scheduled_at] => around 2025-06-15 14:30
end
Generate Between Dates
# Timestamps fall randomly within the range
FakeDataDSL::TimeTravel.between(1.month.ago, Time.current) do
log = FakeDataDSL.generate("AuditLog")
# log[:created_at] is somewhere in the last month
end
# Generate multiple records spread across range
records = FakeDataDSL::TimeTravel.between(1.year.ago, Time.current) do
10.times.map { FakeDataDSL.generate("Transaction") }
end
# Transactions spread randomly across the year
Time Progressions
Linear Progression
Generate records evenly distributed over time:
# 12 monthly reports
reports = FakeDataDSL::TimeTravel.progression("MonthlyReport",
from: 1.year.ago,
to: Time.current,
count: 12
)
reports.each_with_index do |report, i|
puts "#{report[:created_at].strftime('%B %Y')}: $#{report[:revenue]}"
end
# January 2025: $12,345
# February 2025: $13,456
# ...
Custom Intervals
# Daily records
daily = FakeDataDSL::TimeTravel.progression("DailyMetric",
from: 7.days.ago,
to: Time.current,
interval: 1.day
)
# Hourly records
hourly = FakeDataDSL::TimeTravel.progression("HourlyLog",
from: 24.hours.ago,
to: Time.current,
interval: 1.hour
)
# Weekly records
weekly = FakeDataDSL::TimeTravel.progression("WeeklyDigest",
from: 3.months.ago,
to: Time.current,
interval: 1.week
)
With Variations
# Add realistic variation to timestamps
orders = FakeDataDSL::TimeTravel.progression("Order",
from: 1.month.ago,
to: Time.current,
count: 30,
jitter: 6.hours # ±6 hours variation
)
Time Series Data
Generate Time Series
# Stock prices over time
prices = FakeDataDSL::TimeTravel.time_series("StockPrice",
from: 1.year.ago,
to: Time.current,
interval: 1.day,
fields: {
open: { type: :trending, start: 100, volatility: 0.02 },
close: { type: :trending, start: 100, volatility: 0.02 },
volume: { type: :random, range: 1000..10000 }
}
)
Trending Data
# Metrics that grow over time
FakeDataDSL::TimeTravel.time_series("UserMetrics",
from: 1.year.ago,
to: Time.current,
interval: 1.week,
trend: :growth, # :growth, :decline, :stable, :seasonal
growth_rate: 0.05 # 5% weekly growth
)
Seasonal Patterns
# E-commerce orders with seasonal variation
orders = FakeDataDSL::TimeTravel.time_series("Order",
from: 1.year.ago,
to: Time.current,
interval: 1.day,
seasonality: {
pattern: :weekly,
peaks: [5, 6], # Saturday, Sunday
peak_multiplier: 2.5
}
)
Schema Annotations
In DSL Files
# db/schemas/historical_order.dsl
HistoricalOrder:
@time_travel -365.days..-1.day
id: uuid
created_at: timestamp
shipped_at: timestamp(after: created_at, within: 7.days)
delivered_at: timestamp(after: shipped_at, within: 14.days)
Relative Timestamps
# Timestamps that depend on each other
Order:
id: uuid
created_at: timestamp
confirmed_at: timestamp(after: created_at, within: 1.hour)
shipped_at: timestamp(after: confirmed_at, within: 3.days)
delivered_at: timestamp(after: shipped_at, within: 7.days)
When time traveling:
FakeDataDSL::TimeTravel.at(30.days.ago) do
order = FakeDataDSL.generate("Order")
# order[:created_at] => ~30 days ago
# order[:confirmed_at] => ~30 days ago (within 1 hour of created_at)
# order[:shipped_at] => ~27 days ago (within 3 days of confirmed_at)
# order[:delivered_at] => ~20 days ago (within 7 days of shipped_at)
end
Nested Time Contexts
Time Blocks
FakeDataDSL::TimeTravel.at(1.year.ago) do
old_user = FakeDataDSL.generate("User")
FakeDataDSL::TimeTravel.at(6.months.ago) do
mid_user = FakeDataDSL.generate("User")
FakeDataDSL::TimeTravel.at(Time.current) do
new_user = FakeDataDSL.generate("User")
end
end
end
Progressive Nesting
# User signs up, then creates content over time
FakeDataDSL::TimeTravel.at(1.year.ago) do
user = FakeDataDSL.generate("User")
# User writes posts over the following months
posts = (1..12).map do |month|
FakeDataDSL::TimeTravel.at(1.year.ago + month.months) do
FakeDataDSL.generate("Post", user_id: user[:id])
end
end
end
Integration with Scenarios
FakeDataDSL::Scenarios.define(:historical_activity) do
# User from a year ago
let(:veteran_user) {
FakeDataDSL::TimeTravel.at(1.year.ago) do
create(:user, name: "Veteran User")
end
}
# Their posts over time
let(:posts) {
FakeDataDSL::TimeTravel.progression("Post",
from: 11.months.ago,
to: Time.current,
count: 24
).map { |p| p.merge(user_id: veteran_user[:id]) }
}
# Recent new user
let(:new_user) {
FakeDataDSL::TimeTravel.at(1.week.ago) do
create(:user, name: "New User")
end
}
end
Time Zone Handling
Explicit Time Zones
# Generate in specific timezone
FakeDataDSL::TimeTravel.at(Time.current, zone: "America/New_York") do
event = FakeDataDSL.generate("Event")
# event[:start_time] is in Eastern time
end
# Generate in user's timezone
FakeDataDSL::TimeTravel.at(Time.current, zone: user.time_zone) do
notification = FakeDataDSL.generate("Notification")
end
Multiple Zones
# Generate events across timezones
offices = ["America/New_York", "Europe/London", "Asia/Tokyo"]
events = offices.map do |zone|
FakeDataDSL::TimeTravel.at(Time.current, zone: zone) do
FakeDataDSL.generate("Meeting", timezone: zone)
end
end
Testing Patterns
Historical Data Tests
RSpec.describe "ReportGenerator" do
it "generates monthly reports" do
# Create historical data
FakeDataDSL::TimeTravel.progression("Order",
from: 3.months.ago,
to: Time.current,
count: 90
).each { |order| Order.create!(order) }
report = ReportGenerator.monthly_summary(2.months.ago)
expect(report.total_orders).to be > 0
end
end
Time-Sensitive Features
RSpec.describe "SubscriptionExpiry" do
it "expires subscriptions correctly" do
# Create subscription that expires today
subscription = FakeDataDSL::TimeTravel.at(31.days.ago) do
Subscription.create!(
FakeDataDSL.generate("Subscription", duration: 30.days)
)
end
expect(subscription).to be_expired
end
end
Aging Data
RSpec.describe "DataRetention" do
it "purges old records" do
# Create records at various ages
old_record = FakeDataDSL::TimeTravel.at(2.years.ago) do
AuditLog.create!(FakeDataDSL.generate("AuditLog"))
end
recent_record = FakeDataDSL::TimeTravel.at(1.month.ago) do
AuditLog.create!(FakeDataDSL.generate("AuditLog"))
end
DataRetention.purge_old_records!
expect { old_record.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(recent_record.reload).to be_present
end
end
API Reference
TimeTravel.at
FakeDataDSL::TimeTravel.at(time, = {}, &block)
Parameters:
time- Time/DateTime/Date to travel tooptions[:zone]- Time zone name (default: system)block- Block where all timestamps are relative totime
Returns: Result of the block
TimeTravel.between
FakeDataDSL::TimeTravel.between(start_time, end_time, = {}, &block)
Parameters:
start_time- Start of the time rangeend_time- End of the time rangeoptions[:distribution]- :uniform (default), :weighted_recent, :weighted_oldblock- Block where timestamps fall within range
Returns: Result of the block
TimeTravel.progression
FakeDataDSL::TimeTravel.progression(schema_name, = {})
Parameters:
schema_name- Name of the schema to generateoptions[:from]- Start time (required)options[:to]- End time (required)options[:count]- Number of recordsoptions[:interval]- Fixed interval between recordsoptions[:jitter]- Random variation for timestampsoptions[:overrides]- Field overrides
Returns: Array of generated records
TimeTravel.time_series
FakeDataDSL::TimeTravel.time_series(schema_name, = {})
Parameters:
schema_name- Name of the schemaoptions[:from]- Start timeoptions[:to]- End timeoptions[:interval]- Time between data pointsoptions[:trend]- :growth, :decline, :stable, :seasonaloptions[:growth_rate]- Rate of change per intervaloptions[:seasonality]- Seasonal pattern configurationoptions[:fields]- Field-specific configurations
Returns: Array of time series data points
Configuration
FakeDataDSL::TimeTravel.configure do |config|
# Default time zone
config.default_zone = "UTC"
# Default jitter for progressions
config.default_jitter = 0
# Whether to freeze time during generation
config.freeze_time = true
# Hook for before/after time travel
config.before_travel = ->(time) { Rails.logger.debug "Traveling to #{time}" }
config.after_travel = ->(time) { Rails.logger.debug "Returned from #{time}" }
end
Best Practices
1. Use Relative Times
# Good: Relative times adapt to when tests run
FakeDataDSL::TimeTravel.at(30.days.ago) { ... }
# Avoid: Hardcoded dates become stale
FakeDataDSL::TimeTravel.at(Date.new(2025, 1, 1)) { ... }
2. Be Explicit About Relationships
# Good: Clear temporal relationships
Order:
created_at: timestamp
shipped_at: timestamp(after: created_at) # Explicit dependency
# Avoid: Implicit relationships that may not hold
Order:
created_at: timestamp
shipped_at: timestamp # Could be before created_at!
3. Test Edge Cases
# Test around daylight saving time
FakeDataDSL::TimeTravel.at(Time.new(2025, 3, 9, 2, 30, 0, "-05:00")) do
# DST transition edge case
end
# Test at year boundaries
FakeDataDSL::TimeTravel.at(Time.new(2025, 12, 31, 23, 59, 59)) do
# Year-end edge case
end
Troubleshooting
Timestamps Not Affected
# Make sure you're using the block
FakeDataDSL::TimeTravel.at(1.month.ago) do
FakeDataDSL.generate("User") # ✓ Affected
end
FakeDataDSL.generate("User") # ✗ Not affected (outside block)
Time Zone Confusion
# Always be explicit when time zones matter
FakeDataDSL::TimeTravel.at(
Time.current.in_time_zone("America/New_York"),
zone: "America/New_York"
) do
# Clear time zone handling
end