Skip to main content

Testing date formatting with date-fns-tz and different timezones

Override the TZ environment variable in your tests.

I was reading some code which formatted dates using the format function from the date-fns-tz library. Here’s an example:

import { format } from "date-fns-tz"

/* formatDate returns the given date as a date string, with a 12-hour
 * timestamp and the timezone. Example: Jan 2, 2006 - 10:04 PM GMT. */
export function formatDate(date: Date): string {
  return format(date, "MMM d, y - p z")
}

If I tested the code in Chrome by changing the browser timezone, I could see it behaving correctly – the displayed time would change to match my current timezone.

I wanted to write an automated test to check this behaviour.

Option 1: Pass the timezone to format() in OptionsWithTZ

The format() function accepts an optional third argument options: OptionsWithTZ, which can include a timezone. If I allowed passing a timezone to formatDate(), I could pass it to format().

However, that would mean changing the function. This code didn’t have any existing tests, and I don’t like changing code at the same time I add tests – it’s too easy to introduce a change unexpectedly, and codify the wrong behaviour in your new tests.

I prefer to add tests that check the existing behaviour, merge them to main, and only then start changing the implementation.

Option 2: Mock the TZ environment variable

If you don’t give format() an explicit timezone, it guesses one based on your environment. In my Node tests, it was enough to mock the value of the TZ environment variable with different timezones, and watch the value change.

Here’s an example using vitest:

import { afterEach, describe, expect, it, vi } from "vitest"
import { formatDate } from "./dates"

describe("formatDate", () => {
  const d = new Date("2006-01-02T15:04:05-0700")
   
  const testCases: { tz: string, expected: string | Regexp }[] = [
    { tz: "America/Los_Angeles", expected: /Jan 2, 2006 - 2:04 PM (GMT-8|PST)/ },
    { tz: "Europe/London",       expected: "Jan 2, 2006 - 10:04 PM GMT"        },
    { tz: "Asia/Kolkata",        expected: "Jan 3, 2006 - 3:34 AM GMT+5:30"    },
    { tz: "Pacific/Auckland",    expected: "Jan 3, 2006 - 11:04 AM GMT+13"     },
  ]

  afterEach(() => vi.unstubAllEnvs())

  it.each(testCases)("formats date for $tz as $expected", ({ tz, expected }) => {
    vi.stubEnv("TZ", tz)
    expect(formatDate(d)).toMatch(expected)
  })
})

Notes: