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!