Hooks
Loading "Handle Test Side Effects"
Run locally for transcripts
One day, our Peter the Project Manager comes to us with a great idea to improve the app. He suggests that we wish our users a happy day as a part of the greeting message (a bit of kindness goes a long way). That sounds easy enough, and so we change the
greet()
function to reflect that suggestion:export function greet(name: string) {
const weekday = new Date().toLocaleDateString('en-US', { weekday: 'long' })
return `Hello, ${name}! Happy, ${weekday}.`
}
Since the intention behind the code has changed (now it also includes the day of the week), we should adjust the relevant tests to capture that:
test('returns a greeting message for the given name', () => {
expect(greet('John')).toBe('Hello, John! Happy, Monday.')
})
The issue with this test is that it will only pass on Mondays! That won't do. We need a deterministic test, no matter where or when we run it. To fix this, let's first understand why this happens.
Our
greet()
function constructs a Date
to extract a weekday from it. This is a perfectly valid logic but it's also a side effect. The values that Date
returns are not predictable—they change based on, well, the current date! We need to account for that change in our test by, somehow, freezing the current date and making it return the same value on every test run.The usage ofDate
in this case is just an example of a side effect your code may introduce. Whatever it is, the handling of such side effects is often the same.
To handle side effects such as this one, tests often have the concept of "hooks". Hooks are functions that allow you to execute arbitrary code before, during, and after the test run. The purpose of hooks is to help you prepare the environment that runs your tested code.
🐨 In this exercise, you'd have to implement two new global functions (hooks) in
setup.js
:beforeAll()
, which accepts a callback argument and runs it before all tests;afterAll()
, which accepts a callback argument and runs it after the tests are done.
beforeExit
event of the Node.js process as the indicator that all tests are done.Once you do, add a
beforeAll()
hook to the greet.test.js
to patch the globalThis.Date
constructor and return a fixed date from it. Make sure to undo the patch after the tests to keep the test environment clean. And, of course, make sure the tests are passing.Feel free to use the following snippet to mock the
Date
constructor:const OriginalDate = globalThis.Date
beforeAll(() => {
globalThis.Date = new Proxy(globalThis.Date, {
construct: () => new OriginalDate('2024-01-01'),
})
})
afterAll(() => {
globalThis.Date = OriginalDate
})
📜 The Proxy API in JavaScript allows you to spy on any object. In our case, we will spy on the constructor calls to theDate
global object and make it return the same date consistently.
The Golden Rule of Assertions
This problem illustrates the importance of the testing setup for the quality of your tests. Our code does what we intended but the tests still fail. When that happens, you know you've got a bad test. Turns out, such tests don't pass The Golden Rule of Assertions:
🦉 It's a fantastic rule to keep in mind when writing tests. Learn more about The Golden Rule of Assertion and how it can help you write better tests every single time.