Testland
Browse all skills & agents

timecop-ruby

Wraps timecop (github.com/travisjeffery/timecop), the Ruby time-mocking gem: Timecop.freeze, Timecop.travel, Timecop.scale (time-speedup), Timecop.return (cleanup), and RSpec-friendly helpers. Use when testing Ruby/Rails code that calls Time / Date / DateTime. Composes dst-transition-reference + iso-8601-vs-rfc-3339-reference.

timecop-ruby

Overview

timecop is the canonical Ruby gem for time mocking. Per github.com/travisjeffery/timecop, it patches Time.now, Date.today, DateTime.now, and Time.new to return controllable values.

When to use

  • RSpec / minitest tests for Ruby/Rails code using Time / Date.
  • Date-based fixtures.
  • Cron / scheduled-job tests.

Authoring

Install

# Gemfile
group :test do
  gem 'timecop'
end
bundle install

Timecop.freeze (snapshot)

require 'timecop'

Timecop.freeze(Time.local(2026, 5, 20, 14, 30)) do
  expect(Time.now.strftime('%Y-%m-%d')).to eq('2026-05-20')
end
# Time auto-restored after block

Timecop.travel (clock continues)

Timecop.travel(Time.local(2026, 12, 31, 23, 59, 0)) do
  sleep 5  # Real sleep
  expect(Time.now).to be_within(6.seconds).of(Time.local(2026, 12, 31, 23, 59, 5))
end

freeze pauses; travel keeps the clock ticking from the offset.

Manual control

Timecop.freeze(Time.local(2026, 5, 20, 14, 30))
# ... test code
Timecop.return  # Restore

Use ensure blocks to guarantee cleanup:

def test_something
  Timecop.freeze(...)
  begin
    # ...
  ensure
    Timecop.return
  end
end

Timecop.scale (time-speedup)

Timecop.scale(3600) do            # 1 real-second = 1 simulated-hour
  start = Time.now
  sleep 1  # 1 sec real = 1hr simulated
  delta = Time.now - start
  expect(delta).to be_within(60).of(3600)
end

DST tests

require 'active_support/time'

Time.zone = 'America/New_York'
Timecop.freeze(Time.zone.local(2026, 3, 8, 2, 30, 0)) do
  # Behaviour depends on Time.zone library — Rails ActiveSupport
  # has known DST-handling
  expect(...).to ...
end

RSpec helper

# spec_helper.rb
RSpec.configure do |config|
  config.after(:each) { Timecop.return }   # Ensure cleanup
end

Rails: rails-controller-testing + timecop

RSpec.describe BookingController do
  it 'rejects past dates' do
    Timecop.freeze(Date.new(2026, 5, 20)) do
      post :create, params: { date: '2026-05-19' }
      expect(response.status).to eq(400)
    end
  end
end

Running

bundle exec rspec
bundle exec ruby -Itest test/

CI integration

jobs:
  ruby-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: ruby/setup-ruby@v1
        with: { bundler-cache: true }
      - run: bundle exec rspec

Anti-patterns

Anti-patternWhy it failsFix
Forget Timecop.returnCross-test contaminationRSpec after-each hook
Use freeze + sleepsleep is real-time; clock stays frozenUse travel or scale
Hardcode Time.zone in testsCross-test config bleedSave/restore tz per test
Test DST without ActiveSupport zoneRuby Time doesn't track zones nativelyUse ActiveSupport Time.zone
Mock Time.now separately from TimecopConflictUse Timecop exclusively
Use Date.today without freezeTests fail at midnightAlways freeze
Treat travel and freeze identicallyDifferent semanticsUse freeze when clock shouldn't advance

Limitations

  • C extensions bypass timecop. Native gems calling clock_gettime aren't patched.
  • No leap-second simulation.
  • Time.zone (ActiveSupport) and Time may diverge. Be explicit which you're testing.
  • Date.parse in Ruby uses real-system locale. Doesn't honor Timecop.

References