Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Timezone Testing Strategy

Overview

This project uses Zonebie for timezone randomization testing. Zonebie is always enabled to ensure our code works correctly regardless of server timezone.

Current Configuration

  • Always Active: Zonebie randomizes Time.zone for each test run (unless overridden)
  • Override: Set ZONEBIE_TZ environment variable to test in a specific timezone
  • Application Default: Production uses Asia/Bangkok timezone

Why Zonebie?

The application serves users across multiple timezones, and the server timezone in different environments might vary:

  • Local dev: Might be your system timezone
  • CI/CD (GitHub Actions): UTC
  • Production: Asia/Bangkok

Zonebie randomizes the timezone for each test run to catch timezone assumptions early, ensuring our code works correctly regardless of where it’s deployed.

Usage

Regular Testing (Random Timezone - Default)

bundle exec rspec
# Zonebie automatically randomizes timezone
# Example output: [Zonebie] Setting timezone: ZONEBIE_TZ="America/Los_Angeles"

Test Specific Timezone

ZONEBIE_TZ="America/New_York" bundle exec rspec
ZONEBIE_TZ="UTC" bundle exec rspec
ZONEBIE_TZ="Europe/London" bundle exec rspec
ZONEBIE_TZ="Asia/Bangkok" bundle exec rspec  # Test with production timezone

When Zonebie Matters

Zonebie catches bugs in:

  • Date/time calculations and comparisons
  • Timezone conversions
  • Time-dependent business logic
  • Date range validations
  • Scheduled job timing
  • Timestamp formatting

Zonebie doesn’t affect:

  • Tests that don’t use time/dates
  • Database queries without time conditions
  • String manipulation
  • Non-temporal business logic

Fixing Timezone-Dependent Tests

If a test fails due to timezone randomization, it likely has timezone assumptions. Here’s how to fix:

Problem: Date/Time Calculations Across Timezone Boundaries

# ❌ BAD: Uses Date.current which changes with Time.zone
let(:today) { Time.now_in_tz(time_zone).to_date }
let!(:auto_extend_add_on) do
  create(:restaurant_add_on, end_date: today)  # Factory uses Date.current
end

Solution 1: Wrap Setup in Time.use_zone

# ✅ GOOD: Ensure consistent timezone for all date calculations
around do |example|
  Time.use_zone('Asia/Bangkok') do
    example.run
  end
end

let(:today) { Time.zone.today }
let!(:auto_extend_add_on) do
  create(:restaurant_add_on, end_date: today)  # Now uses Bangkok timezone
end

Solution 2: Use Explicit Timezone in Factories

# ✅ GOOD: Explicit timezone handling
let(:time_zone) { 'Asia/Bangkok' }
let(:today) { Time.use_zone(time_zone) { Time.zone.today } }
let!(:auto_extend_add_on) do
  Time.use_zone(time_zone) do
    create(:restaurant_add_on, end_date: today)
  end
end

Problem: Hardcoded Time Expectations

# ❌ BAD: Assumes Bangkok timezone
it 'shows correct time' do
  travel_to Time.zone.local(2026, 1, 8, 14, 30, 45) do
    expect(some_time_display).to eq('21:30:45')  # Assumes +07 offset
  end
end

Solution: Use Explicit Timezone

# ✅ GOOD: Explicit timezone handling
it 'shows correct time' do
  Time.use_zone('Asia/Bangkok') do
    travel_to Time.zone.local(2026, 1, 8, 14, 30, 45) do
      expect(some_time_display).to match(/\d{2}:\d{2}:\d{2}/)  # Format check only
    end
  end
end

Problem: Relative Time Comparisons Without Timezone Context

# ❌ BAD: Timecop.freeze without Time.use_zone
Timecop.freeze(Time.zone.parse('2026-01-15 18:40:00')) do
  # Time.zone could be anything due to Zonebie
end

Solution: Always Wrap with Time.use_zone

# ✅ GOOD: Explicit timezone + time freezing
Time.use_zone('Asia/Bangkok') do
  Timecop.freeze(Time.zone.parse('2026-01-15 18:40:00')) do
    # Now we know exactly what timezone we're in
  end
end

Current Test Status

All tests should pass with Zonebie randomization. If you encounter timezone-related failures:

  1. Check if the test uses Time.zone, Date.current, or time calculations
  2. Wrap the test setup and execution in Time.use_zone('Asia/Bangkok')
  3. Ensure factories are created within the same timezone context
  4. Use around blocks for consistent timezone handling across all examples

Previously Failing Tests (Now Fixed)

These tests required timezone fixes and serve as examples:

  • spec/workers/schedule_workers/auto_extend_add_on_sub_worker_spec.rb - Required Time.use_zone wrapper
  • spec/my_lib/seat_availability_spec.rb - Required around block with Time.use_zone
  • spec/my_lib/agents/update_for_owner_spec.rb - Required around block with Time.use_zone

Caveat (June 2026): seat_availability_spec flaked again after the timezone fix, and the random Zonebie timezone in the CI log turned out to be a red herring — the real cause was an AdminSetting value leaking through Rails.cache from another spec. If a spec is already fully pinned with Time.use_zone and still flakes, see Flaky Specs: AdminSetting Cache Leak before reaching for more timezone fixes.

Technical Details

Configuration Files

  • spec/rails_helper.rb: Always loads Zonebie (require 'zonebie/rspec')
  • No conditional loading - Zonebie is always active

How It Works

  1. Zonebie loads automatically when RSpec starts
  2. It randomly selects a timezone and sets Time.zone (unless ZONEBIE_TZ is set)
  3. Each test run gets a different random timezone
  4. Tests must use Time.use_zone to ensure correct timezone handling

Best Practices

✅ Good Timezone Practices

# Always use Time.use_zone for timezone-specific operations
Time.use_zone('Asia/Bangkok') do
  Time.zone.now  # Guaranteed to be Bangkok time
end

# Use around blocks for consistent test timezone
around do |example|
  Time.use_zone('Asia/Bangkok') do
    example.run
  end
end

# Use Time.zone methods instead of Time.now
Time.zone.now   # Respects Time.zone setting
Time.zone.today # Date in current timezone

# Create records within timezone context
Time.use_zone('Asia/Bangkok') do
  create(:restaurant_add_on, end_date: Time.zone.today)
end

# Pass timezone explicitly to HhTime methods
HhTime.time_has_passed?(date, time, 'Asia/Bangkok')

❌ Avoid

# Don't use Date.current without timezone context
let(:today) { Date.current }  # Changes with Time.zone!

# Don't use Time.now (ignores Time.zone)
Time.now  # System time, not timezone-aware

# Don't assume Time.zone is Bangkok
Time.zone.now.hour  # Depends on current Time.zone

# Don't create records without timezone context
create(:restaurant_add_on, end_date: Date.current)  # Fails with Zonebie

# Don't hardcode timezone offsets
time + 7.hours  # What if server timezone changes?

Migration Strategy

For existing tests that fail with Zonebie:

  1. Identify the timezone assumption - Look for Date.current, Time.zone.now, time calculations
  2. Add Time.use_zone wrapper - Wrap test setup and execution
  3. Use around blocks - For consistent timezone across all examples in a context
  4. Test the fix - Run with ZONEBIE_TZ="UTC" and ZONEBIE_TZ="America/New_York"

Gradual Adoption

Zonebie is already enabled by default and working for most tests:

  1. ✅ Phase 1: Zonebie enabled by default (current state)
  2. ✅ Phase 2: Fix timezone-dependent tests (ongoing)
  3. 📋 Phase 3: Document all timezone-sensitive areas
  4. 📋 Phase 4: Add timezone testing guidelines to PR reviews

Resources