CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/fake-clock-testing

Fake clocks / freeze time in tests across every mainstream runtime: freezegun (Python), Jest fake timers + Sinon @sinonjs/fake-timers (JS/TS), timecop (Ruby), java.time.Clock / InstantSource injection (JVM), .NET TimeProvider / FakeTimeProvider, and libfaketime (LD_PRELOAD for any native binary). Covers the language-agnostic discipline - inject or patch the clock, freeze vs tick vs advance vs set-system-time semantics, teardown so fake clocks never leak between tests - plus the shared anti-pattern table (real sleep under a frozen clock, leaked clock state, timezone-dependent assertions). Per-library setup, API, and CI recipes live in references/{python,js,ruby,jvm,dotnet,libfaketime}.md. Use when tests need deterministic control of now(), timers, or timeouts in any language, or when choosing the right fake-clock tool for a stack.

93

Quality

93%

Does it follow best practices?

Impact

Average score across 3 eval scenarios

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

ruby.mdreferences/

Ruby - timecop

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

Install

# Gemfile
group :test do
  gem 'timecop'
end

Timecop.freeze (snapshot) vs Timecop.travel (clock continues)

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

Timecop.travel(Time.local(2026, 12, 31, 23, 59, 0)) do
  sleep 5  # real sleep; travel keeps the clock ticking from the offset
  expect(Time.now).to be_within(6.seconds).of(Time.local(2026, 12, 31, 23, 59, 5))
end

freeze pauses the clock; travel offsets it and lets it keep ticking. They are not interchangeable - use freeze when the clock must not advance.

Manual control and cleanup

Timecop.freeze(Time.local(2026, 5, 20, 14, 30))
# ... test code
Timecop.return    # restore; wrap in ensure when not using the block form

RSpec safety net:

RSpec.configure do |config|
  config.after(:each) { Timecop.return }
end

Timecop.scale (time speed-up)

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

DST tests (Rails / ActiveSupport)

Ruby Time doesn't track zones natively; use ActiveSupport's Time.zone:

require 'active_support/time'

Time.zone = 'America/New_York'
Timecop.freeze(Time.zone.local(2026, 3, 8, 2, 30, 0)) do
  # 02:30 local doesn't exist on this spring-forward date;
  # assert the documented behaviour per dst-transition-reference
end

Save and restore Time.zone per test - it is process-global config.

Rails controller example

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

Anti-patterns

Anti-patternWhy it failsFix
Forget Timecop.returnCross-test contaminationRSpec after-each hook
freeze + sleepSleep is real-time; the frozen clock stays putUse travel or scale
Hardcode Time.zone in testsConfig bleeds across testsSave/restore the zone per test
DST test without ActiveSupport zoneRuby Time has no zone trackingTime.zone + Time.zone.local
Date.today without a freezeTest fails at midnightAlways freeze

Limitations

  • C extensions bypass timecop - native gems calling clock_gettime aren't patched; use libfaketime (libfaketime.md).
  • Time.zone (ActiveSupport) and Time can diverge - be explicit about which the code under test reads.
  • Date.parse uses the real system locale and does not honor Timecop.

References

SKILL.md

tile.json