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
- Die Zeit läuft weiter – Tests, die zu unterschiedlichen Zeiten durchgeführt werden, führen zu unterschiedlichen Ergebnissen
- Komplexität der Zeitzone – DST-Übergänge, Offset-Änderungen, historische Zeitzonendaten
- Randfälle – Schaltsekunden, Jahresgrenzen, ungültige Datumsangaben
- Asynchrone Vorgänge – Timer, Verzögerungen und zeitabhängige Nebenwirkungen
- 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
- Mock Time – Verwenden Sie Jest-Fake-Timer, Freezegun oder Abhängigkeitsinjektion
- Testen Sie die Sommerzeit – Berücksichtigen Sie Spring-Forward- und Fallback-Szenarien
- Zeitzonen testen – Konvertierungen über mehrere Zeitzonen hinweg überprüfen
- Testrandfälle – Jahresgrenzen, Schaltjahre, ungültige Daten
- UTC intern verwenden – In UTC speichern, nur zur Anzeige konvertieren
Empfohlene Werkzeuge
JavaScript:
- Scherz mit
@sinonjs/fake-timers date-fns-tzfür Zeitzonentestsluxonfür umfassendes Datetime-Handling
Python:
pytestfür Test-Frameworkfreezegunfür Zeitverspottungpytzfür Zeitzonentests
Los:
- Abhängigkeitsinjektion mit Zeitschnittstellen
- Tabellengesteuerte Tests zur Parametrisierung