🤔 Are you backing up your git repositories?

Nick Jones

Product-Focused CTO, Founder,
Software Engineer, Indie Hacker

Deterministic testing with Luxon and Jest

When developing software, dealing with dates and timezones can be a challenging task. Luxon, a powerful date and time library for JavaScript and TypeScript, provides excellent support for handling dates, including timezones.

When writing unit tests, ensuring determinism is vital. Deterministic tests produce consistent results regardless of the environment or external factors. However, when working with dates and times, achieving determinism can be challenging.

A common approach to making tests deterministic is to pass in the current date as a variable, commonly named now, into functions. While this technique works in many cases, there are scenarios where calling out to get the current time within a function becomes necessary.

Luxon’s DateTime.now() method let’s us get the current date and time, however, calling DateTime.now() directly within a function can introduce non-determinism and result in unreliable unit tests. To address this issue, Luxon provides the Settings.now property, which allows us to override the default behavior and make it deterministic.

Settings.now accepts a function with the signature () => number, which should return the desired fixed value representing the current time. By controlling this value, we can ensure consistent behavior during testing.

To demonstrate how to write deterministic unit tests with Luxon and Jest, let’s consider an example scenario. Imagine we have a function called getFormattedCurrentDate that returns the current date in a specific format. We want to test this function while ensuring determinism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { DateTime, Settings } from 'luxon';

function getFormattedCurrentDate(): string {
  const now = DateTime.now();
  return now.toFormat('yyyy-MM-dd');
}

let originalNow: typeof Settings['now'];
const now = DateTime.utc(2023, 1, 28, 9, 30, 0, 0).toMillis();

describe('getFormattedCurrentDate', () => {
  beforeEach(() => {
    originalNow = Settings.now;
    Settings.now = () => now;
  });

  afterEach(() => {
    Settings.now = originalNow;
  });

  it('returns the formatted current date', () => {
    const formattedDate = getFormattedCurrentDate();
    expect(formattedDate).toBe('2023-01-28');
  });
});

In the example above, we utilize Jest’s beforeEach and afterEach functions to override Luxon’s Settings.now property. Within the beforeEach callback, we set Settings.now to a fixed value representing the desired current time for the test. After each test, in the afterEach callback, we restore the original Settings.now value.

By encapsulating the test-specific behavior within the beforeEach and afterEach blocks, we ensure that each test runs in isolation and avoids any side effects.

Jest provides two sets of functions for test setup and teardown: beforeEach and beforeAll, as well as afterEach and afterAll. Understanding the distinctions between these pairs is essential when writing tests.

The beforeEach and afterEach functions are executed before and after each individual test, respectively. These functions are particularly useful when you want to ensure a clean state for each test case, allowing tests to be independent and isolated from one another.

On the other hand, beforeAll and afterAll are executed once before the entire test suite and after the suite, respectively. These functions are suitable for setup and teardown tasks that need to be performed only once, saving unnecessary repetition and improving test suite performance.

By choosing the appropriate setup and teardown functions based on the requirements of your tests, you can ensure the desired behavior and optimize the execution flow.

Happy testing!