Skip to content

Commit e1c839f

Browse files
committed
Slightly improve heart tests
- Get rid of the global isActive mock; in particular the way it shadows local ones seemed sketchy. - No need for requireActual from my testing. - Reword the comment for why we need setImmediate. - Add the setImmediate to another test that seemed to only pass because of an await on the timer call which is not actually a promise but had the side effect of yielding. - Always set fake/real timers in the before/after handlers and never in individual tests.
1 parent 7dfd685 commit e1c839f

1 file changed

Lines changed: 51 additions & 28 deletions

File tree

test/unit/node/heart.test.ts

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { logger } from "@coder/logger"
22
import { readFile, writeFile, stat, utimes } from "fs/promises"
3+
import { setImmediate } from "timers"
34
import { Heart } from "../../../src/node/heart"
45
import { clean, mockLogger, tmpdir } from "../../utils/helpers"
56

6-
const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)
7-
87
describe("Heart", () => {
98
const testName = "heartTests"
109
let testDir = ""
@@ -15,19 +14,23 @@ describe("Heart", () => {
1514
await clean(testName)
1615
testDir = await tmpdir(testName)
1716
})
18-
beforeEach(() => {
19-
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
20-
})
17+
2118
afterAll(() => {
2219
jest.restoreAllMocks()
2320
})
21+
22+
beforeEach(() => {
23+
heart = new Heart(`${testDir}/shutdown.txt`, jest.fn().mockResolvedValue(true))
24+
})
25+
2426
afterEach(() => {
2527
jest.resetAllMocks()
2628
jest.useRealTimers()
2729
if (heart) {
2830
heart.dispose()
2931
}
3032
})
33+
3134
it("should write to a file when given a valid file path", async () => {
3235
// Set up heartbeat file with contents
3336
const text = "test"
@@ -42,7 +45,7 @@ describe("Heart", () => {
4245

4346
expect(fileContents).toBe(text)
4447

45-
heart = new Heart(pathToFile, mockIsActive(true))
48+
heart = new Heart(pathToFile, jest.fn().mockResolvedValue(true))
4649
await heart.beat()
4750
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
4851
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
@@ -51,31 +54,30 @@ describe("Heart", () => {
5154
const fileStatusAfterEdit = await stat(pathToFile)
5255
expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(0)
5356
})
57+
5458
it("should log a warning when given an invalid file path", async () => {
55-
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
59+
heart = new Heart(`fakeDir/fake.txt`, jest.fn().mockResolvedValue(false))
5660
await heart.beat()
5761
expect(logger.warn).toHaveBeenCalled()
5862
})
63+
5964
it("should be active after calling beat", async () => {
6065
await heart.beat()
6166

6267
const isAlive = heart.alive()
6368
expect(isAlive).toBe(true)
6469
})
70+
6571
it("should not be active after dispose is called", () => {
6672
heart.dispose()
6773

6874
const isAlive = heart.alive()
6975
expect(isAlive).toBe(false)
7076
})
77+
7178
it("should beat twice without warnings", async () => {
72-
// Use fake timers so we can speed up setTimeout
73-
jest.useFakeTimers()
74-
heart = new Heart(`${testDir}/hello.txt`, mockIsActive(true))
79+
heart = new Heart(`${testDir}/hello.txt`, jest.fn().mockResolvedValue(true))
7580
await heart.beat()
76-
// we need to speed up clocks, timeouts
77-
// call heartbeat again (and it won't be alive I think)
78-
// then assert no warnings were called
7981
jest.runAllTimers()
8082
expect(logger.warn).not.toHaveBeenCalled()
8183
})
@@ -84,82 +86,103 @@ describe("Heart", () => {
8486
describe("heartbeatTimer", () => {
8587
const testName = "heartbeatTimer"
8688
let testDir = ""
89+
8790
beforeAll(async () => {
8891
await clean(testName)
8992
testDir = await tmpdir(testName)
9093
mockLogger()
9194
})
95+
9296
afterAll(() => {
9397
jest.restoreAllMocks()
9498
})
99+
95100
beforeEach(() => {
96101
jest.useFakeTimers()
97102
})
103+
98104
afterEach(() => {
99105
jest.resetAllMocks()
100106
jest.clearAllTimers()
101107
jest.useRealTimers()
102108
})
109+
103110
it("should call isActive when timeout expires", async () => {
104-
const isActive = true
105-
const mockIsActive = jest.fn().mockResolvedValue(isActive)
111+
const mockIsActive = jest.fn().mockResolvedValue(true)
106112
const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
107113
await heart.beat()
108114
jest.advanceTimersByTime(60 * 1000)
109115
expect(mockIsActive).toHaveBeenCalled()
110116
})
117+
111118
it("should log a warning when isActive rejects", async () => {
112-
const errorMsg = "oh no"
113-
const error = new Error(errorMsg)
119+
const error = new Error("oh no")
114120
const mockIsActive = jest.fn().mockRejectedValue(error)
115121
const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
116122
await heart.beat()
117123
jest.advanceTimersByTime(60 * 1000)
118-
// Yield a real macrotask so the callback's rejection handler can run first
119-
// (fake timers stub the global setImmediate).
120-
await new Promise((resolve) => jest.requireActual("timers").setImmediate(resolve))
121-
122124
expect(mockIsActive).toHaveBeenCalled()
123-
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
125+
126+
// The timer callback waits on mockIsActive, so we need to yield to let the
127+
// callback finish.
128+
await new Promise((resolve) => setImmediate(resolve))
129+
130+
expect(logger.warn).toHaveBeenCalledWith(error.message)
124131
})
125132
})
126133

127134
describe("stateChange", () => {
128135
const testName = "stateChange"
129136
let testDir = ""
130137
let heart: Heart
138+
131139
beforeAll(async () => {
132140
await clean(testName)
133141
testDir = await tmpdir(testName)
134142
mockLogger()
135143
})
144+
136145
afterAll(() => {
137146
jest.restoreAllMocks()
138147
})
148+
149+
beforeEach(() => {
150+
jest.useFakeTimers()
151+
})
152+
139153
afterEach(() => {
140154
jest.resetAllMocks()
155+
jest.useRealTimers()
141156
if (heart) {
142157
heart.dispose()
143158
}
144159
})
160+
145161
it("should change to alive after a beat", async () => {
146-
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
162+
const mockIsActive = jest.fn().mockResolvedValue(true)
163+
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
147164
const mockOnChange = jest.fn()
148165
heart.onChange(mockOnChange)
166+
149167
await heart.beat()
150168

151169
expect(mockOnChange.mock.calls[0][0]).toBe("alive")
152170
})
171+
153172
it("should change to expired when not active", async () => {
154-
jest.useFakeTimers()
155-
heart = new Heart(`${testDir}/shutdown.txt`, () => new Promise((resolve) => resolve(false)))
173+
const mockIsActive = jest.fn().mockResolvedValue(false)
174+
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
156175
const mockOnChange = jest.fn()
157176
heart.onChange(mockOnChange)
177+
158178
await heart.beat()
179+
jest.advanceTimersByTime(60 * 1000)
180+
expect(mockIsActive).toHaveBeenCalled()
181+
182+
// The timer callback waits on the isActive promise, so we need to yield to
183+
// let the callback finish.
184+
await new Promise((resolve) => setImmediate(resolve))
159185

160-
await jest.advanceTimersByTime(60 * 1000)
161186
expect(mockOnChange.mock.calls[1][0]).toBe("expired")
162-
jest.clearAllTimers()
163-
jest.useRealTimers()
164187
})
165188
})

0 commit comments

Comments
 (0)