timezone-test-matrix-builder

Use when a codebase needs time zone, DST and leap-year test coverage derived from its own date and time code. Finds where times are stored, computed, displayed, scheduled or billed, picks the edge cases that matter and writes test stubs using the language's fake-clock library.

/plugin install qa-time@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)
Skill
Plugin
qa-time
Version
v1.2.27

Overview

Time-related bugs are scattered across the codebase - storage, display, business logic, scheduled jobs. The test matrix needs to systematically exercise the canonical edge cases at each touchpoint.

When to use

  • Introducing time-test coverage to a new codebase.
  • After a time-related incident (DST bug, leap-day failure).
  • Migrating from one timezone library to another.
  • Periodic audit of time-handling.

Step 1 - Inventory time touchpoints

# Generic
grep -rn 'datetime\|Date\|Instant\|time.time\|moment\.\|dayjs\|chrono' \
  --include='*.{py,js,ts,java,kt,rb,go,rs,cs}' .

# Per-language
grep -rn 'datetime.now\|datetime.utcnow\|Date.now\|Instant.now' .
grep -rn 'tz\|timezone\|zoneinfo\|ZoneId' .
grep -rn 'cron\|schedule' .

Categorise each match:

CategoryExamplesTest needs
StorageDB columns; serialised datesRFC 3339 round-trip (offset or Z preserved)
Business logicAge calculation; duration; expiryDST, leap-day, monotonic
DisplayUser-facing datesPer-user-tz formatting
Cron / scheduledPeriodic jobsDST transition behaviour per dst-transition-reference
BillingPeriod boundariesDST + month-end + leap year
Audit / loggingTimestamp emissionMonotonic; leap-second tolerance
External APIThird-party datetime stringsTolerant parsing

Step 2 - Per-category test catalog

For each touchpoint, pull the test cases matching its category from references/test-catalog.md, which lists the storage, business-logic, cron, billing, and display tests to exercise.

Step 3 - Per-language test harness

All per-language fake-clock harnesses live in fake-clock-testing:

Languagefake-clock-testing reference
Python (freezegun)references/python.md
JS/TS (Jest + Sinon fake timers)references/js.md
Ruby (timecop)references/ruby.md
JVM (Clock / InstantSource)references/jvm.md
.NET (TimeProvider / FakeTimeProvider)references/dotnet.md
C / native binary (libfaketime)references/libfaketime.md

Step 4 - Build the matrix

For each (category, touchpoint, language) cell, generate test stubs:

# tests/time/matrix.yaml
matrix:
  - touchpoint: BillingService.createCharge
    category: billing
    tests:
      - dst-fall-back
      - leap-year-feb-29
      - month-end-rollover
      - timezone-multi-tenant
    language: java
    harness: jvm-clock-injection

  - touchpoint: ScheduledTask.runDaily
    category: cron
    tests:
      - dst-spring-forward
      - dst-fall-back
      - leap-day
    language: ruby
    harness: timecop

  # ...

Step 5 - Emit per-cell test files

# tests/time/test_billing_service.py
import pytest
from freezegun import freeze_time
from billing import BillingService

@freeze_time("2024-02-29T00:00:00Z")
def test_billing_handles_leap_day():
    charge = BillingService.create_charge_for_month(2024, 2)
    assert charge.days_in_period == 29

@freeze_time("2025-02-28T00:00:00Z")
def test_billing_handles_non_leap_february():
    charge = BillingService.create_charge_for_month(2025, 2)
    assert charge.days_in_period == 28

@freeze_time("2026-11-01T05:30:00Z")  # Just past fall-back in NY
def test_billing_period_spans_dst_fall_back():
    # Period from Nov 1 00:00 to Nov 2 00:00 in New_York
    # is 25 hours of UTC due to fall-back
    period = BillingService.month_period(year=2026, month=11, zone="America/New_York")
    assert period.duration.total_seconds() == 30 * 24 * 3600 + 3600  # 1 extra hour

Step 6 - Run and validate the generated tests

Verify before recording coverage: run the emitted files (pytest tests/time/, mvn test, etc.) and assert every DST and leap-day case produces its expected pass (or the expected failure for a known-bug reproduction). If a case errors instead of asserting, the fake-clock wiring is wrong - fix the harness mapping (Step 3) or add the missing TZ / zone for local-time cases - and re-run until the matrix is green.

Step 7 - Coverage doc

# Time Test Matrix Coverage

## Touchpoints covered

| Service | Category | Tests | File |
|---|---|---|---|
| BillingService | billing | leap-day, dst-fall-back, month-end | tests/time/test_billing.py |
| ScheduledTask | cron | dst-spring-forward, leap-day | tests/time/test_cron.py |
| API serialiser | storage | rfc-3339-round-trip | tests/time/test_api_format.py |

## Coverage gaps

- BillingService - leap-second tolerance: deferred (low likelihood)
- Display layer: per-user-TZ rendering - manual QA only

## How to add a new touchpoint

1. Run inventory grep (Step 1).
2. Categorise (Step 2).
3. Update matrix.yaml.
4. Generate test from template (per Step 5).

Worked example

Adding leap-day coverage to a Python BillingService.create_charge_for_month: inventory (Step 1) categorises it as billing, whose catalog rows call for month-end-across-leap-year, DST-window, and multi-tenant-timezone tests. Python maps to freezegun (fake-clock-testing references/python.md, Step 3), so the emitted tests/time/test_billing_service.py (Step 5) freezes 2024-02-29 asserting days_in_period == 29 and 2025-02-28 asserting 28. Running the file (Step 6) confirms both pass, and the coverage doc (Step 7) then records BillingService's billing category as covered - the leap-year February boundary is now exercised on every run.

Anti-patterns

Anti-patternWhy it failsFix
Test only the happy pathTime bugs are edge casesDST + leap-day mandatory
Live system time in testsAnnual / quarterly flakesAlways fake-clock
One mega-test for all time edge casesFailures opaquePer-category, per-touchpoint
Skip storage round-tripSchema drift / serialiser bug hidesRFC 3339 round-trip everywhere
Test in UTC onlyMisses local-zone DST / display bugsPer-zone testing
Hardcoded dates that ageRe-write needed annuallyUse relative dates or fake clock
No coverage docGaps invisibleStep 7
Ignore display-layerReal users see wrong datesEven if manual, document the manual coverage

References

  • IANA Time Zone Database: www.iana.org/time-zones (opens in new window).
  • Companion catalog: dst-transition-reference (DST bug classes; leap seconds in its references/leap-seconds.md).
  • Per-language harnesses: fake-clock-testing (freezegun, Jest/Sinon fake timers, timecop, JVM Clock injection, .NET TimeProvider, libfaketime).
  • Cross-plugin (cron): cron-job-test-author (qa-async-jobs).

The test cases each touchpoint category should exercise. Pick the rows matching the category assigned during inventory (Step 1).

Storage tests

TestPattern
Round-trip RFC 3339parse → emit → parse → assert equal
Round-trip via JSONserialise object → deserialise → assert
Microsecond precision preserved.123456Z survives DB store/load
Zone information preserved or normalized to UTCDocument the policy

Business-logic tests

TestPattern
DST spring-forwardSchedule at 02:30 local on transition day; verify behaviour
DST fall-backSame 01:30 local appearing twice; verify ordering
Leap day Feb 29"1 year from Feb 29 2024" → Feb 28 2025 (Per ICU)
Year-end rollover"Tomorrow" on Dec 31
Month-endJan 31 + 1 month = Feb 28 / 29 (per library)
Negative durationsOperations on "5 minutes ago"
Leap second toleranceCode uses monotonic time per dst-transition-reference references/leap-seconds.md

Cron tests

TestPattern
Daily 02:30 EST cron on spring-forwardDoesn't fire OR fires at 03:30 (per cron spec)
Daily 01:30 EST cron on fall-backFires once vs twice (per spec)
Monthly on Feb 29 (non-leap year)Fires on Feb 28 OR not at all
Weekly cron crossing DST1-hour offset for one week

Billing tests

TestPattern
Billing on month-end across leap yearFeb 28 vs Feb 29 handling
Billing window across DSTHour gain / loss in the period
Pro-ration calculationAcross DST boundary
Multi-tenant timezone varianceSame wall-clock hour ≠ same UTC

Display tests

TestPattern
Per-user TZ formattingUser in Asia/Tokyo sees 09:00 JST; user in Europe/London sees 00:00 GMT
ISO 8601 vs human-readableLocalised display ≠ wire format
24h vs 12h conventionPer user locale
Relative time ("2 hours ago")Per system locale