A Checklist for Testing Dates, Time and Time Zones
Dates and time are one of those areas that look simple and break in sneaky ways: the bug doesn’t show up right away — it surfaces six months later, on the night the clocks change, on February 29, or for a user in another time zone. Here’s a practical checklist of what to test.
Storage and model
- Everything on the backend is UTC. The time zone is applied only for display. Storing local time without a zone is an almost guaranteed bug.
- “Naive” datetimes (no tz) are a red flag: when compared, sorted, or written to the DB they get silently interpreted differently.
- Separate a point in time from a calendar date. A “date of birth” and a “deadline at 23:59” are different things: one has no time/zone, the other does.
- Check that the API returns time in ISO 8601 with an offset (
2026-08-18T14:30:00Zor+02:00), not an ambiguous local form.
Time zones
- User in one TZ, server in another, data created in a third — test the whole chain.
- User changed time zone (flight, VPN, manual change in settings) — past events must not “shift”.
- Offsets aren’t always whole hours: India (+05:30), Nepal (+05:45), parts of Australia.
- Are you using an up-to-date time zone database (IANA/tzdata)? Countries change their rules; hardcoding an offset is a bug.
Daylight saving (DST)
- Non-existent time: springing forward skips an hour (e.g., 02:00→03:00). What does the system do with an event at 02:30?
- Duplicated time: falling back, 02:30 happens twice. Which of the two moments is meant?
- An event in the transition “gap” — a reminder/cron/booking exactly on the missing hour.
- Countries switch DST on different dates (and not all switch at all) — don’t hardcode “like in the US/EU”.
Display and input formats
04/05/2026— is that May 4 or April 5? Check DD/MM vs MM/DD by locale, for output and input.- 12h vs 24h,
AM/PM, midnight00:00vs12:00 AM, noon. - Local separators and order; month/day names in the user’s language.
- First day of the week (Mon or Sun — locale-dependent) in date pickers.
Boundaries (where it all breaks)
- Midnight and crossing midnight; events exactly at
00:00and23:59:59. - End of month/year; the “31st” in months that don’t have one.
- February 29 (leap year) — and “+1 year” from Feb 29.
- “+1 month” from January 31 — Feb 28/29 or March 3? Pin down the expected behavior.
- Epoch and overflow: Unix time 0 (1970), the year 2038 problem (32-bit
time_t), very large/negative dates.
Calculations and differences
- The difference between dates across a DST transition — a day may not be 24 hours (23 or 25).
- Age, “days remaining”, business days (excluding weekends/holidays by country).
- Adding durations: “in 90 days” vs “in 3 months” — these are different results.
- Comparing times from different zones — normalize to a single moment (UTC), don’t compare them “as is”.
Relative time
- “5 minutes ago”, “tomorrow”, “in 2 hours” — test at boundaries (23:59 → “tomorrow”).
- The client’s clock is wrong/skewed — don’t trust the device time for critical logic (tokens, deadlines); the source of truth is the server.
- Live-updating relative labels (after a minute, “just now” should become “1 min ago”).
How to reproduce
- Change the device/OS time zone and language — the fastest way to catch display bugs.
- Freeze/mock the clock in automated tests (pin “now”), otherwise the test flakes near midnight/year boundary.
- Keep a set of dangerous dates as test data: Feb 29, DST-change night (forward and back), Dec 31 23:59, a +05:45 zone, the year 2038.
- Run in at least two or three time zones (UTC, one with DST, one with a half-hour offset).
In short
- Store in UTC, apply the zone only for display; “naive” datetimes are a bug.
- DST creates non-existent and duplicated time — test events in those windows.
- Date formats are ambiguous (04/05) — test output and input by locale.
- Boundaries: midnight, end of month, Feb 29, “+1 month” from the 31st, 2038.
- Don’t trust the client’s clock; the server is the source of time.
- Reproduce by changing the device TZ + freezing the clock + a set of dangerous dates.