Upcoming Changes to the Test Runner in Node 22
Colin J. Ihrig
It's almost that time again. Node 22 is currently scheduled to be released in about a week and a half. I'd like to briefly mention some of the changes that have gone into Node's test runner (node:test
) recently because they are unlikely to get called out in the official release notes.
- PR 52003. Ensure that the
before
hook is always run. Prior to this change, thebefore
hook would not run if there were no tests. It was updated to always run in order to match the behavior ofafter
. - PR 52115. When a test is skipped, the
beforeEach
andafterEach
hooks should also be skipped. This change implements that behavior. - PR 52239. This PR changes the order that
afterEach
hooks are run in. Prior to this change, a parent'safterEach
hooks would run before the current test'safterEach
hooks. This behavior has now been fixed so that the current test'safterEach
hooks run first. It is worth noting thatafterEach
was the only hook impacted by this bug. - PR 51996. Abort tests that create
uncaughtException
events. Prior to this change, the test runner would clean up after the test. However, the test was never actually aborted, so the test runner would continue waiting for it to finish. Now, theuncaughtException
handler aborts the test so that everything proceeds normally. - PR 52020. This changes focuses on making the reported total execution time of the test runner more accurate. In some cases it was previously possible to overwrite the overall start time. This would lead to an inaccurate total execution time. The test runner no longer allows the start time to be overwritten.
- PR 52010 and PR 52036. These changes are focused on better handling of test locations. When a test fails (or if you consume the test events directly), the test runner reports the filename, line number, and column number where the test is located. After these changes, the location is correctly reported when Node's
--enable-source-maps
flag is used. The test runner and reporters were also updated to handleundefined
test locations, which can occur if you are using the test runner in the REPL. - PR 52060. The test runner's experimental code coverage feature now respects source maps. Prior to this change, code coverage did not handle source maps at all.
- PR 52117. When a test or suite is marked as TODO, it should never impact whether or not the test is run. It should only impact the way failures are interpreted. Prior to this change, it was possible for a suite to be incorrectly skipped when marked as TODO. This change corrects that behavior. By the way, if you were ever curious what TODO tests are all about, PR 52204 added a section to the API docs explaining how they work.
- PR 52130. This change allows code coverage to be reported multiple times when using watch mode.
- PR 52127. This change adds a
suite()
function and makes the existingdescribe()
function an alias of it. Now, you can usetest()
andit()
to create tests, andsuite()
anddescribe()
to create suites. - PR 52038. This change adds a new
--test-force-exit
CLI flag andforceExit
option torun()
. This forces the test runner to exit once it has processed all tests, regardless of any active handles keeping the event loop alive. I personally dislike this change very much because you should figure out why the event loop is still alive and fix the underlying problem. However, I do understand the use case and have been on teams that really do need this functionality... so here we are. - PR 52287. This change disables the
highWatermark
on the stream that generates the test reporter events. This stream is an object mode stream, so the defaulthighWatermark
was only 16. This limit gets hit fairly quickly, resulting in a lot of overhead. However, thehighWatermark
doesn't actually make sense for this particular stream, so it is safe to disable. This lead to significant performance improvements in the test runner. - PR 52185. This is a small performance improvement. Prior to this change, every top level test awaited the same
Promise
. Now, only the first top level test incurs that small overhead. - PR 52408. This is another performance improvement. This one improves the performance of
AbortSignal
s, which are used a good bit in the test runner. - PR 52221, PR 52326, and PR 52488. This change completely removes filtered tests from the test runner output. In this context, filtering refers to tests that are not run because of the
--test-only
or--test-name-pattern
flags. Prior to this change, filtered tests would simply be treated as skipped tests. However, for large test suites this would result in a lot of unhelpful output. Now, these tests are treated as if they never existed at all. This is technically a breaking change, but I would personally be in favor of backporting it to older release lines. - PR 52092 and PR 52296. These changes are the first steps toward addressing a major pain point with Node's test runner -
only
tests. In order to runonly
tests, you need to attach theonly
flag to the appropriate tests and pass the--test-only
CLI flag. You would also need to set theonly
flag on any suites containing the tests you want to run. After these changes, you no longer need to take that last step. Hopefully, if PR 51579 ever lands, you won't need to pass--test-only
anymore either unless you're using test isolation (not to mention the addition performance improvements).
More on performance
I mentioned some performance enhancements. In Node 22, the test runner should indeed be faster (although Amdahl's law still applies). Let's run a very quick performance test on my laptop comparing Node's main
branch to v21.7.3. This is not a perfect comparison, but it's good enough for our purposes here. Here is the code:
'use strict'
const { test } = require('node:test');
for (let i = 0; i < 100_000; ++i) {
test(`test ${i + 1}`);
}
On my laptop, Node v21.7.3 took roughly 86 seconds. Node's main
branch took roughly 8 seconds. Note, these numbers were obtained by running node test.js
without the --test
CLI flag. When --test
is used, there is additional overhead and these numbers jump to 95 and 18 seconds, respectively.
Filtering should also be significantly faster. The same code run with --test-only
took 84 seconds on Node v21. It makes sense that the old filtering implementation took about as long as a no-op test since filtered tests were really just a no-op skipped test. Node's main
branch took about 5 seconds in this benchmark.
Conclusion
I am excited for some of the improvements coming with the test runner. I am also really excited for some of the other changes currently in open pull requests, such as the option to run all tests in the same process. Of course, I'm also still keeping an eye toward my 2024 Wishlist for Node's Test Runner.