Tutorial

Zeitstempel-Teststrategien: Vollständiger Leitfaden

Einführung

Das Testen von zeitabhängigem Code ist bekanntermaßen schwierig. Die Zeit fließt kontinuierlich, Zeitzonen ändern sich und Randfälle wie Sommerzeitübergänge erzeugen komplexe Szenarien. Dieses Tutorial bietet umfassende Strategien zum Testen von zeitstempelbezogenem Code, einschließlich Mock-Time, Testen von Randfällen und Sicherstellen der Zuverlässigkeit über Zeitzonen hinweg.

Warum das Testen von Zeitstempeln schwierig ist

Schlüsselherausforderungen

  1. Die Zeit läuft weiter – Tests, die zu unterschiedlichen Zeiten durchgeführt werden, führen zu unterschiedlichen Ergebnissen
  2. Komplexität der Zeitzone – DST-Übergänge, Offset-Änderungen, historische Zeitzonendaten
  3. Randfälle – Schaltsekunden, Jahresgrenzen, ungültige Datumsangaben
  4. Asynchrone Vorgänge – Timer, Verzögerungen und zeitabhängige Nebenwirkungen
  5. Umgebungsabhängigkeiten – Systemzeitzone, Gebietsschemaeinstellungen

Häufige Probleme

// ❌ Non-deterministic test - fails at certain times
test('event is in the future', () => {
  const event = new Date('2024-12-31T23:59:59Z');
  expect(event > new Date()).toBe(true); // Fails after Dec 31, 2024!
});

// ❌ Timezone-dependent test - fails in different timezones
test('gets current day', () => {
  const day = new Date().getDay();
  expect(day).toBe(2); // Only passes on Tuesdays in local timezone!
});

Strategie 1: Scheinzeit

JavaScript mit Jest

Abhängigkeiten installieren:

npm install --save-dev jest @sinonjs/fake-timers

Grundlegende Zeitverspottung

describe('Timestamp Tests with Mocked Time', () => {
  beforeEach(() => {
    // Set fake time to a fixed date
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2024-01-15T12:00:00Z'));
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  test('getCurrentTimestamp returns mocked time', () => {
    const timestamp = Date.now();
    expect(timestamp).toBe(new Date('2024-01-15T12:00:00Z').getTime());
  });

  test('time advances with runTimersToTime', () => {
    const start = Date.now();

    jest.advanceTimersByTime(1000); // Advance 1 second

    const end = Date.now();
    expect(end - start).toBe(1000);
  });
});

Fortgeschritten: Geplante Vorgänge testen

function scheduleReport(callback, delayMs) {
  setTimeout(() => {
    const timestamp = new Date().toISOString();
    callback({ timestamp, report: 'Generated' });
  }, delayMs);
}

test('schedules report correctly', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2024-01-15T12:00:00Z'));

  const callback = jest.fn();
  scheduleReport(callback, 5000);

  // Fast-forward time
  jest.advanceTimersByTime(5000);

  expect(callback).toHaveBeenCalledWith({
    timestamp: '2024-01-15T12:00:05.000Z',
    report: 'Generated'
  });

  jest.useRealTimers();
});

Python mit Pytest und Freezegun

Abhängigkeiten installieren:

pip install pytest freezegun

Einfaches Einfrieren der Zeit

import pytest
from datetime import datetime
from freezegun import freeze_time

@freeze_time("2024-01-15 12:00:00")
def test_current_timestamp():
    """Test with frozen time."""
    now = datetime.now()
    assert now.year == 2024
    assert now.month == 1
    assert now.day == 15
    assert now.hour == 12

@freeze_time("2024-01-15 12:00:00")
def test_timestamp_calculation():
    """Test calculations with frozen time."""
    from datetime import timedelta

    now = datetime.now()
    future = now + timedelta(hours=1)

    assert future.hour == 13
    assert (future - now).total_seconds() == 3600

Zeitreisetests

from freezegun import freeze_time
from datetime import datetime, timedelta

def test_time_travel():
    """Test by moving through time."""
    initial_time = datetime(2024, 1, 15, 12, 0, 0)

    with freeze_time(initial_time) as frozen_time:
        assert datetime.now() == initial_time

        # Move forward 1 hour
        frozen_time.move_to(initial_time + timedelta(hours=1))
        assert datetime.now().hour == 13

        # Move forward 1 day
        frozen_time.move_to(initial_time + timedelta(days=1))
        assert datetime.now().day == 16

Entscheiden Sie sich für Zeitschnittstellen

Verwenden Sie in Go die Abhängigkeitsinjektion für testbaren Timecode:

package timeutil

import "time"

// TimeProvider interface allows mocking
type TimeProvider interface {
    Now() time.Time
}

// RealTime uses actual system time
type RealTime struct{}

func (RealTime) Now() time.Time {
    return time.Now()
}

// MockTime allows setting fixed time
type MockTime struct {
    CurrentTime time.Time
}

func (m *MockTime) Now() time.Time {
    return m.CurrentTime
}

// EventScheduler uses TimeProvider
type EventScheduler struct {
    timer TimeProvider
}

func (es *EventScheduler) IsEventInFuture(eventTime time.Time) bool {
    return eventTime.After(es.timer.Now())
}

// Test file
func TestEventScheduler(t *testing.T) {
    mockTime := &MockTime{
        CurrentTime: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC),
    }

    scheduler := &EventScheduler{timer: mockTime}

    futureEvent := time.Date(2024, 1, 15, 13, 0, 0, 0, time.UTC)
    pastEvent := time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)

    if !scheduler.IsEventInFuture(futureEvent) {
        t.Error("Future event should be in future")
    }

    if scheduler.IsEventInFuture(pastEvent) {
        t.Error("Past event should not be in future")
    }
}

Strategie 2: Sommerzeitübergänge testen

Spring Forward testen

import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';

describe('DST Spring Forward Tests', () => {
  test('handles missing hour correctly', () => {
    // March 10, 2024, 2:00 AM doesn't exist in New York
    const timezone = 'America/New_York';

    // Try to create 2:30 AM (missing hour)
    const missingHour = new Date('2024-03-10T02:30:00');
    const utcTime = zonedTimeToUtc(missingHour, timezone);
    const localTime = utcToZonedTime(utcTime, timezone);

    // Should be adjusted to 3:30 AM
    expect(localTime.getHours()).toBe(3);
    expect(localTime.getMinutes()).toBe(30);
  });

  test('duration calculation on spring forward day', () => {
    const start = new Date('2024-03-10T00:00:00-05:00'); // EST
    const end = new Date('2024-03-10T23:59:59-04:00');   // EDT

    const hours = (end - start) / 3600000;
    expect(hours).toBeCloseTo(23, 0); // Day is only 23 hours
  });
});

Test-Fallback

import pytest
import pytz
from datetime import datetime

def test_fall_back_duplicate_hour():
    """Test handling of duplicate hour during fall back."""
    ny_tz = pytz.timezone('America/New_York')

    # November 3, 2024, 1:30 AM occurs twice
    # First occurrence (EDT)
    first = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=True)

    # Second occurrence (EST)
    second = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=False)

    # Should be 1 hour apart
    diff = (second - first).total_seconds()
    assert diff == 3600  # 1 hour

def test_fall_back_day_duration():
    """Test that fall back day is 25 hours."""
    ny_tz = pytz.timezone('America/New_York')

    start = ny_tz.localize(datetime(2024, 11, 3, 0, 0, 0))
    end = ny_tz.localize(datetime(2024, 11, 3, 23, 59, 59))

    duration_hours = (end - start).total_seconds() / 3600
    assert duration_hours > 24  # Day is longer than 24 hours

Strategie 3: Zeitzonenkonvertierungen testen

Parametrisierte Tests

import pytest
import pytz
from datetime import datetime

@pytest.mark.parametrize("utc_time,timezone,expected_hour", [
    ("2024-01-15 12:00:00", "America/New_York", 7),   # EST: UTC-5
    ("2024-01-15 12:00:00", "Europe/London", 12),      # GMT: UTC+0
    ("2024-01-15 12:00:00", "Asia/Tokyo", 21),         # JST: UTC+9
    ("2024-01-15 12:00:00", "Australia/Sydney", 23),   # AEDT: UTC+11
])
def test_timezone_conversion(utc_time, timezone, expected_hour):
    """Test UTC to timezone conversion."""
    utc = pytz.UTC
    tz = pytz.timezone(timezone)

    dt_utc = datetime.strptime(utc_time, "%Y-%m-%d %H:%M:%S").replace(tzinfo=utc)
    dt_local = dt_utc.astimezone(tz)

    assert dt_local.hour == expected_hour

Strategie 4: Randfälle testen

Jahresgrenzen

describe('Year Boundary Tests', () => {
  beforeEach(() => jest.useFakeTimers());
  afterEach(() => jest.useRealTimers());

  test('handles new year transition', () => {
    // Set time to 1 second before new year
    jest.setSystemTime(new Date('2023-12-31T23:59:59Z'));

    const beforeYear = new Date().getFullYear();
    expect(beforeYear).toBe(2023);

    // Advance 2 seconds
    jest.advanceTimersByTime(2000);

    const afterYear = new Date().getFullYear();
    expect(afterYear).toBe(2024);
  });

  test('calculates days correctly across year boundary', () => {
    const dec31 = new Date('2023-12-31T12:00:00Z');
    const jan1 = new Date('2024-01-01T12:00:00Z');

    const days = (jan1 - dec31) / (1000 * 60 * 60 * 24);
    expect(days).toBe(1);
  });
});

Schaltjahr

import pytest
from datetime import datetime

@pytest.mark.parametrize("year,is_leap", [
    (2020, True),   # Divisible by 4
    (2021, False),  # Not divisible by 4
    (2000, True),   # Divisible by 400
    (1900, False),  # Divisible by 100 but not 400
])
def test_leap_year_detection(year, is_leap):
    """Test leap year detection."""
    try:
        # Feb 29 exists only in leap years
        datetime(year, 2, 29)
        assert is_leap
    except ValueError:
        assert not is_leap

def test_leap_year_calculations():
    """Test calculations involving leap years."""
    # 2020 is a leap year (366 days)
    year_start = datetime(2020, 1, 1)
    year_end = datetime(2020, 12, 31, 23, 59, 59)

    days = (year_end - year_start).days
    assert days == 365  # .days doesn't include the last partial day

    # 2021 is not a leap year (365 days)
    year_start = datetime(2021, 1, 1)
    year_end = datetime(2021, 12, 31, 23, 59, 59)

    days = (year_end - year_start).days
    assert days == 364

Strategie 5: Integrationstests

Test mit Echtzeitzonendatenbank

// Test that uses actual timezone data
describe('Real Timezone Tests', () => {
  test('correctly handles all US DST transitions in 2024', () => {
    const transitions = [
      { date: '2024-03-10T07:00:00.000Z', type: 'spring forward' },
      { date: '2024-11-03T06:00:00.000Z', type: 'fall back' }
    ];

    transitions.forEach(({ date, type }) => {
      const transitionTime = new Date(date);

      // Test that we can detect the transition
      const before = new Date(transitionTime.getTime() - 3600000);
      const after = new Date(transitionTime.getTime() + 3600000);

      const beforeOffset = before.getTimezoneOffset();
      const afterOffset = after.getTimezoneOffset();

      if (type === 'spring forward') {
        expect(beforeOffset).toBeGreaterThan(afterOffset);
      } else {
        expect(beforeOffset).toBeLessThan(afterOffset);
      }
    });
  });
});

Best Practices

1. Intern immer UTC verwenden

class EventManager {
  createEvent(localTime, timezone) {
    // Store in UTC
    const utcTime = zonedTimeToUtc(localTime, timezone);
    return {
      utc_timestamp: utcTime.getTime(),
      display_timezone: timezone
    };
  }

  displayEvent(event, timezone) {
    // Convert to display timezone only when needed
    const localTime = utcToZonedTime(event.utc_timestamp, timezone);
    return localTime;
  }
}

// Test
test('events store and display correctly', () => {
  const manager = new EventManager();

  // Create event in New York time
  const event = manager.createEvent(
    new Date('2024-01-15T15:00:00'),
    'America/New_York'
  );

  // Display in Tokyo time
  const tokyoTime = manager.displayEvent(event, 'Asia/Tokyo');

  // Should be next day in Tokyo (14 hours ahead)
  expect(tokyoTime.getDate()).toBe(16);
});

2. Datengrenzen testen

import pytest
from datetime import datetime

def test_timestamp_boundaries():
    """Test minimum and maximum timestamp values."""
    # Unix timestamp epoch
    epoch = datetime.fromtimestamp(0)
    assert epoch.year == 1970

    # Year 2038 problem (32-bit signed int overflow)
    max_32bit = datetime.fromtimestamp(2147483647)
    assert max_32bit.year == 2038

    # Negative timestamps (before epoch)
    before_epoch = datetime.fromtimestamp(-86400)  # 1 day before epoch
    assert before_epoch.year == 1969

3. Testvorrichtungen verwenden

import pytest
from datetime import datetime
import pytz

@pytest.fixture
def fixed_time():
    """Provide a fixed time for tests."""
    return datetime(2024, 1, 15, 12, 0, 0, tzinfo=pytz.UTC)

@pytest.fixture
def dst_transition_dates():
    """Provide DST transition dates."""
    return {
        'spring_forward': datetime(2024, 3, 10, 2, 0, 0),
        'fall_back': datetime(2024, 11, 3, 2, 0, 0)
    }

def test_with_fixtures(fixed_time, dst_transition_dates):
    """Use fixtures in tests."""
    assert fixed_time.year == 2024
    assert len(dst_transition_dates) == 2

Häufige Fallstricke

Vermeiden:

  • Testen mit aktueller Zeit (new Date() ohne Mocking) – Vorausgesetzt, dass die Tests in einer bestimmten Zeitzone ausgeführt werden
  • Ignorieren von DST-Übergängen in Tests
  • Festcodierte Daten, die ungültig werden
  • Randfälle (Schaltjahre, Jahresgrenzen) werden nicht getestet.

Machen Sie:

  • Scheinen Sie in Tests immer die Zeit
  • Testen Sie über mehrere Zeitzonen hinweg
  • Beziehen Sie DST-Übergangsdaten in Testfälle ein
  • Verwenden Sie nach Möglichkeit relative Daten
  • Testen Sie sowohl typische als auch Randfälle

Zusammenfassung

Wichtige Teststrategien

  1. Mock Time – Verwenden Sie Jest-Fake-Timer, Freezegun oder Abhängigkeitsinjektion
  2. Testen Sie die Sommerzeit – Berücksichtigen Sie Spring-Forward- und Fallback-Szenarien
  3. Zeitzonen testen – Konvertierungen über mehrere Zeitzonen hinweg überprüfen
  4. Testrandfälle – Jahresgrenzen, Schaltjahre, ungültige Daten
  5. UTC intern verwenden – In UTC speichern, nur zur Anzeige konvertieren

Empfohlene Werkzeuge

JavaScript:

  • Scherz mit @sinonjs/fake-timers
  • date-fns-tz für Zeitzonentests
  • luxon für umfassendes Datetime-Handling

Python:

  • pytest für Test-Framework
  • freezegun für Zeitverspottung
  • pytz für Zeitzonentests

Los:

  • Abhängigkeitsinjektion mit Zeitschnittstellen
  • Tabellengesteuerte Tests zur Parametrisierung

Verwandte Ressourcen