Back to Blog

How to test time-dependent code without losing your mind

April 15, 2026
3 min read
Written by ConvertTime Team
testingtime-zonesdstdeveloperssoftware-engineering
Code that depends on the current time, time zones, or DST shifts is famously hard to test. A test that calls `new Date()` or `time.time()` directly is non-deterministic and can break randomly. The trick is to make time injectable — controllable from your test code — so you can simulate specific moments precisely. For converting Unix timestamps in test setup, the [epoch converter](/calculators/epoch) helps with manual reasoning. ## The problem with `now()` ```python def is_business_hours(): now = datetime.now() return 9 <= now.hour < 17 def test_is_business_hours(): assert is_business_hours() == True # This test passes only between 9 AM and 5 PM! # Fails at night, on weekends, on different time zones ``` This kind of test breaks randomly. Worse, it can pass in CI but fail on a developer's machine, or vice versa. ## The fix: inject time Make the function take time as a parameter: ```python def is_business_hours(current_time=None): now = current_time or datetime.now() return 9 <= now.hour < 17 def test_is_business_hours(): monday_morning = datetime(2026, 5, 18, 10, 0, 0) assert is_business_hours(monday_morning) == True monday_evening = datetime(2026, 5, 18, 22, 0, 0) assert is_business_hours(monday_evening) == False ``` Now the test is deterministic. You control the input. ## The clock abstraction For more complex code, abstract the entire clock: ```python class Clock: def now(self): return datetime.now(timezone.utc) class FakeClock(Clock): def __init__(self, fixed_time): self.time = fixed_time def now(self): return self.time def advance(self, delta): self.time += delta # In production code class OrderProcessor: def __init__(self, clock=None): self.clock = clock or Clock() def process(self, order): if self.clock.now() > order.expires_at: raise OrderExpiredError() # ... # In tests fake_clock = FakeClock(datetime(2026, 5, 21, 14, 30, tzinfo=timezone.utc)) processor = OrderProcessor(clock=fake_clock) order = Order(expires_at=datetime(2026, 5, 21, 15, 0, tzinfo=timezone.utc)) processor.process(order) # OK, not expired fake_clock.advance(timedelta(hours=1)) with pytest.raises(OrderExpiredError): processor.process(order) ``` This pattern is used in many production systems. ## Library tools Several libraries provide "freeze time" functionality: **Python: freezegun** ```python from freezegun import freeze_time import datetime @freeze_time("2026-05-21 14:30:00") def test_with_frozen_time(): assert datetime.datetime.now() == datetime.datetime(2026, 5, 21, 14, 30) ``` **Python: pytest-freezegun** — pytest plugin **JavaScript: jest-mock-extended** with `jest.useFakeTimers()` **JavaScript: sinon** for sinon timers **Ruby: timecop** gem **Rust: tokio's pause/resume timer** **Go: vcr / clock package** These libraries replace `now()` calls with a fixed value during the test. When the test ends, normal time resumes. ## Testing DST transitions The bug you most want to catch is DST handling. Test for: ```python @freeze_time("2026-03-08 06:30:00 UTC") # 1:30 AM EST, before spring forward def test_before_spring_dst(): # Test code that runs before DST shift pass @freeze_time("2026-03-08 07:30:00 UTC") # 3:30 AM EDT, after spring forward def test_after_spring_dst(): # Test code that runs after DST shift pass @freeze_time("2026-11-01 05:30:00 UTC") # 1:30 AM EDT (first occurrence) def test_before_fall_dst(): pass @freeze_time("2026-11-01 06:30:00 UTC") # 1:30 AM EST (second occurrence) def test_after_fall_dst(): pass def test_scheduling_during_missing_hour(): """The hour 2:00-2:59 AM US Eastern doesn't exist on 2026-03-08.""" naive = datetime(2026, 3, 8, 2, 30) # Behavior should be defined — exception, fallback, or skip? # Verify your code does the right thing ``` Catching these in tests prevents production surprises. ## Testing leap year edge cases ```python def test_age_calculation_leap_year_feb_29(): """Test that age calculations work for Feb 29 birthdays.""" birth = datetime(2000, 2, 29) today = datetime(2026, 5, 21) age = (today - birth).days // 365 # Test specific edge cases for leap year handling def test_birthday_on_feb_29_in_non_leap_year(): """Feb 29 birthdays in non-leap years.""" birth = datetime(2000, 2, 29) # Test handling of "March 1" or "Feb 28" rule for non-leap years ``` ## Testing time zone conversions ```python def test_utc_to_eastern_summer(): utc = datetime(2026, 7, 15, 14, 30, 0, tzinfo=ZoneInfo("UTC")) eastern = utc.astimezone(ZoneInfo("America/New_York")) assert eastern.hour == 10 # 14:30 UTC = 10:30 EDT (summer) def test_utc_to_eastern_winter(): utc = datetime(2026, 1, 15, 14, 30, 0, tzinfo=ZoneInfo("UTC")) eastern = utc.astimezone(ZoneInfo("America/New_York")) assert eastern.hour == 9 # 14:30 UTC = 9:30 EST (winter) ``` Test both seasonal cases. A bug that only happens in winter is invisible to a test that runs in summer. ## Mocking date-dependent business logic For complex scenarios, dependency-inject the entire date logic: ```python class BusinessRules: def __init__(self, clock): self.clock = clock def is_q4(self): month = self.clock.now().month return month >= 10 def is_holiday_week(self): today = self.clock.now() return today.month == 12 and today.day >= 24 # Test with various dates rules = BusinessRules(FakeClock(datetime(2026, 11, 15, tzinfo=timezone.utc))) assert rules.is_q4() is True assert rules.is_holiday_week() is False rules = BusinessRules(FakeClock(datetime(2026, 12, 25, tzinfo=timezone.utc))) assert rules.is_q4() is True assert rules.is_holiday_week() is True ``` ## Testing recurring jobs For cron-style scheduled jobs, test: - The job runs when scheduled - The job doesn't run between scheduled times - The job handles missed executions gracefully (e.g., after a crash) - The job handles DST shifts correctly ```python @freeze_time("2026-05-21 08:55:00") # Before scheduled time def test_job_before_scheduled(): # Job shouldn't run yet assert job_runner.should_run("0 9 * * *") is False @freeze_time("2026-05-21 09:00:00") # Exactly scheduled def test_job_at_scheduled(): # Job should run assert job_runner.should_run("0 9 * * *") is True @freeze_time("2026-05-21 09:01:00") # 1 minute after def test_job_just_after_scheduled(): # Behavior depends on your scheduler — does it catch up or skip? pass ``` ## Testing time-based pricing/charges For systems that charge based on time: ```python def test_subscription_renewal_dst_shift(): """Subscription renewing on March 8, 2026 (DST spring forward day).""" # Verify the renewal happens at the right UTC moment pass def test_billing_period_across_dst(): """A billing period spanning a DST shift.""" # Verify the billing period is correctly calculated pass ``` These tests are tedious but catch real bugs. ## CI/CD considerations For your CI pipeline: **Set system time zone to UTC.** Predictable, no DST shifts. Tests run consistently. **Set tzdata to a known version.** Some tests may be sensitive to tzdata changes. **Run on multiple time zones in matrix.** Use CI matrix to run tests with different system time zones, catching environment-dependent bugs. **Test with clock skew.** Some libraries can simulate slight clock differences between mocked and real time. ## Common testing patterns ### "Run this test only on first day of month" ```python @pytest.mark.skipif(datetime.now().day != 1, reason="Only run on first day") def test_first_day_logic(): pass ``` This is *bad*. It only runs once per month. Better: ```python @freeze_time("2026-05-01") def test_first_day_logic(): # Now this runs every CI run pass ``` ### "Test against multiple specific dates" ```python @pytest.mark.parametrize("test_date", [ "2026-01-01", # Year boundary "2026-02-29" if False else None, # Skip if not leap "2026-03-08", # Spring DST "2026-11-01", # Fall DST "2026-12-31", # Year end ]) def test_dates(test_date): if test_date is None: pytest.skip("Test date not applicable") # ... ``` ### "Verify timezone-aware behavior" ```python def test_business_hours_across_zones(): # Define what "9-5" means assert is_business_hours( datetime(2026, 5, 21, 14, 0, tzinfo=ZoneInfo("America/New_York")) ) is True # 2 PM Eastern = within 9-5 assert is_business_hours( datetime(2026, 5, 21, 14, 0, tzinfo=ZoneInfo("Asia/Tokyo")) ) is True # 2 PM Tokyo = within Tokyo's 9-5 ``` ## FAQ ### Why is testing time-dependent code so hard? Because the "current time" is non-deterministic. Mocking it requires either dependency injection or library magic. Both require explicit setup. ### Should I freeze time in production? No. Freeze time is for testing only. Production systems use real time. ### What's the difference between freezegun and dependency injection? Freezegun replaces `time.time()` and similar globally during a test. Dependency injection passes a clock to your code. Dependency injection is cleaner but requires refactoring; freezegun works without refactoring. ### How do I test cron expressions? Use a cron parser library and test that specific datetimes match (or don't match) your expression. Libraries like `croniter` (Python) handle this. ### What about distributed systems? Test each component's time handling locally. For cross-component tests, mock at component boundaries or use a shared "test clock." ### How thorough do my time tests need to be? Depends on your domain. Critical financial systems? Very thorough. Internal tooling? Moderate. Marketing landing pages? Less. ## Bottom line Test time-dependent code by injecting time as a parameter or dependency. Use libraries like freezegun or dedicated clocks. Specifically test DST shifts, leap years, year boundaries, and time zone conversions. Run tests in UTC for consistency. For converting timestamps in test setup, the [epoch converter](/calculators/epoch) handles them.

Share this article

Related Articles