Back to blog
Test Automation#timestamps#date-testing#timezone#iso8601#unix-timestamp#test-automation#qa-tools

Timestamp Testing: How to Handle Dates and Times in QA Automation

A comprehensive guide to testing timestamps and date-time handling in your APIs and applications. Covers Unix timestamps, ISO 8601, timezone bugs, clock manipulation in tests, and the most common date-related defects in production.

InnovateBits7 min read

Date and time bugs are among the most common — and most embarrassing — issues that escape to production. A subscription that expires on New Year's Eve behaves differently in different time zones. A "today's orders" report includes tomorrow's orders in certain regions. A recurring event fires twice or not at all around daylight saving time transitions.

These bugs are preventable with methodical timestamp testing. This guide explains how timestamps work, where the common failure points are, and how to build date-time assertions into your test suite.


Understanding the timestamp formats you'll encounter

Unix timestamps

A Unix timestamp is the number of seconds elapsed since January 1, 1970, 00:00:00 UTC (the "Unix epoch"). It's unambiguous, timezone-agnostic, and universally supported.

1704067200  →  2024-01-01 00:00:00 UTC

APIs and databases often use two variants:

  • Seconds: 10-digit integer (e.g., 1704067200)
  • Milliseconds: 13-digit integer (e.g., 1704067200000) — used by JavaScript's Date.now()

The Timestamp Converter tool converts between these and human-readable formats instantly, making it useful when you're reading timestamps from API responses or CI logs during debugging.

ISO 8601

ISO 8601 is the international standard for date-time strings. Modern APIs should use this format:

2024-01-01T00:00:00Z          ← UTC (Z = zero offset)
2024-01-01T05:30:00+05:30     ← IST (India Standard Time)
2024-01-01T00:00:00.000Z      ← UTC with milliseconds

The T separates date and time. The offset (Z, +05:30, -08:00) specifies the timezone. When the offset is Z, the time is UTC.

Common bug: missing timezone offset

When an API returns a timestamp without a timezone indicator:

{ "createdAt": "2024-01-01 08:30:00" }

This is ambiguous. Is it UTC? Local server time? The user's timezone? APIs that return timezone-naive timestamps will behave differently for users in different timezones and will produce subtly wrong results in any system that converts or compares timestamps.

Test: assert that all timestamp fields include a timezone offset.

const body = await response.json()
// ISO 8601 with timezone (Z or +/-HH:MM)
expect(body.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/)

The most common date-time bugs

1. Off-by-one day (timezone boundary)

A user in UTC+5:30 creates a record at 11:00 PM local time. The server stores it as UTC, which is 5:30 PM the previous day. When the user queries "today's records", the server uses UTC "today" — and the record appears to be from yesterday.

Test scenario: create a record at a time that crosses a UTC day boundary and assert it appears in the correct date group.

// Simulate a record created at 11:30 PM IST (= 6:00 PM UTC the same calendar day in IST
// but the UTC date might differ depending on timezone offset direction)
const recordTime = '2024-01-15T23:30:00+05:30'
await request.post('/api/records', { data: { timestamp: recordTime } })
 
// Query for records on 2024-01-15 — the record must appear regardless of server timezone
const response = await request.get('/api/records?date=2024-01-15&timezone=Asia/Kolkata')
const records = await response.json()
expect(records.some(r => r.timestamp === recordTime)).toBe(true)

2. Daylight saving time (DST) transitions

On DST transition days, some local times either don't exist (spring forward) or occur twice (fall back). A scheduler that fires at "2:30 AM local time" will skip that time in spring and fire twice in autumn — or throw an error if the time doesn't exist.

Test: verify that scheduling, expiry, and date calculation logic handles DST transitions correctly.

// In a timezone that observes DST (e.g., America/New_York)
// Spring forward: 2:00 AM becomes 3:00 AM — 2:30 AM doesn't exist
// Fall back: 2:00 AM happens twice
 
const dstTransitionDate = '2024-03-10' // Spring forward in US Eastern
const response = await request.post('/api/subscriptions', {
  data: {
    startDate: dstTransitionDate,
    renewalTime: '02:30',
    timezone: 'America/New_York'
  }
})
// Should succeed without error, even though 2:30 AM doesn't exist that day
expect(response.status()).toBe(201)

3. Leap year handling

February 29 exists only in leap years. Date arithmetic that adds one year to a date will fail on Feb 29:

2024-02-29 + 1 year = ???

The correct answer is 2025-02-28 (last day of February in a non-leap year), but some systems throw an exception, some return 2025-03-01, and some silently return invalid data.

Test: create records on Feb 29 and verify that annual calculations produce valid dates.

// Only valid in 2024 (leap year)
const leapDate = '2024-02-29'
const response = await request.post('/api/subscriptions', {
  data: { startDate: leapDate, plan: 'annual' }
})
const body = await response.json()
 
// Renewal date should be a valid date — 2025-02-28 is most common correct answer
expect(body.nextRenewalDate).toMatch(/^2025-02-2[78]$/) // Feb 28 is correct

4. Unix timestamp overflow (Year 2038)

32-bit signed integers overflow on January 19, 2038, at 03:14:07 UTC. Systems still using 32-bit Unix timestamps will fail. While this is several years away, it's worth checking that any new system you test uses 64-bit timestamp storage.

Test: assert that your API accepts and returns timestamps beyond 2038.

const futureDate = '2040-01-01T00:00:00Z'
const response = await request.post('/api/events', {
  data: { scheduledAt: futureDate }
})
expect(response.status()).toBe(201)
const body = await response.json()
expect(body.scheduledAt).toBe(futureDate)

5. Inconsistent timestamp formats across endpoints

One endpoint returns "2024-01-15T10:30:00Z" (ISO 8601), another returns "January 15, 2024 10:30:00" (locale-formatted), another returns 1705312200 (Unix seconds). This inconsistency causes parsing errors in clients and makes automated comparison impossible.

Test: verify all endpoints return timestamps in the same format.

const ISO_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/
 
const endpoints = [
  '/api/users/usr_123',
  '/api/orders/ord_456',
  '/api/events/evt_789',
]
 
for (const endpoint of endpoints) {
  const response = await request.get(endpoint)
  const body = await response.json()
  
  // All timestamp fields must use ISO 8601
  for (const field of ['createdAt', 'updatedAt', 'deletedAt']) {
    if (body[field]) {
      expect(body[field]).toMatch(ISO_PATTERN)
    }
  }
}

Clock manipulation in tests

Testing time-dependent logic requires controlling what "now" means in your tests. You can't wait 7 days for a subscription to expire.

Playwright clock API

Playwright provides a clock fixture to freeze or control time:

test('subscription expires after trial period', async ({ page, clock }) => {
  // Set the clock to a specific time
  await clock.setSystemTime(new Date('2024-01-01T00:00:00Z'))
  
  // Start a 7-day trial
  await page.goto('/start-trial')
  await page.click('[data-testid="start-trial"]')
  
  // Jump forward 8 days
  await clock.setSystemTime(new Date('2024-01-09T00:00:00Z'))
  await page.reload()
  
  // Trial should now be expired
  await expect(page.locator('[data-testid="trial-expired"]')).toBeVisible()
})

Jest fake timers

test('reminder fires 24 hours before appointment', () => {
  jest.useFakeTimers()
  jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
  
  const scheduler = new ReminderScheduler()
  scheduler.schedule({ appointmentAt: '2024-01-16T10:00:00Z' })
  
  // Advance 23 hours — reminder should not have fired
  jest.advanceTimersByTime(23 * 60 * 60 * 1000)
  expect(mockSendReminder).not.toHaveBeenCalled()
  
  // Advance 1 more hour — now at 24 hours before, reminder should fire
  jest.advanceTimersByTime(60 * 60 * 1000)
  expect(mockSendReminder).toHaveBeenCalledTimes(1)
  
  jest.useRealTimers()
})

Using the Timestamp Converter in QA work

The Timestamp Converter tool is useful during manual investigation of date-related issues:

Decoding API responses: when an API returns "exp": 1704209856, paste it into the converter to immediately see January 2, 2024 at 08:30:56 UTC without mental arithmetic.

Verifying expiry windows: check whether a token's exp claim is set correctly — paste the iat and exp values and confirm the window is what you expect (e.g., exactly 1 hour, not 1 hour and 30 seconds due to a configuration error).

Cross-timezone verification: enter a timestamp and change the timezone selector to verify that the same instant in time displays correctly across multiple zones.

Live clock reference: enable live clock mode to see the current Unix timestamp in real time — useful when you need to compare a freshly-generated token's iat claim against the actual current time.

Free newsletter

Stay ahead in AI-driven QA

Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.