Repository navigation
How to run the Node built-in testrunner for TypeScript files inside a specific directory? #3902
Description
Activity
Did you try injecting a TypeScript processor as a command line parameter for
node, yet?E.g.
node --require @swc/register[...] ornode --require ts-node/register[...]Reacted by Tanguy BERNARD, Brandon Bennett, Otman, Aleksei Kozadaev, Stephen Mathieson, Jeff Wainwright, Paul M Dorr, Samuel Umoh, Gábor Soós, JHT and 3 moreIn case it helps anyone who runs across this issue, here is how I currently execute tests written in TypeScript with the new node test runner and esbuild-kit/tsx:
npm install -D tsx
Add the following npm script entry:
"test": "node --loader tsx --test test/**/*Test.ts"
Let me know if this is helpful.
Edit: See the reply below by @scottwillmoore for an excellent tip to make this work more robustly across platforms.
Reacted by Jason Galea, Lukas van Driel, Andreas Thomas, Alexandre Cardim, Helio Frota, Chris Haynes, Eric Crosson, Joon Park, jtuchel, Moshe Atlow and 77 moreReacted by Jaid, Mordechai Dror, zengsun, Arnab Datta, Kostya, Daniel Kehlibarov, Sam Carlton, Jamie Tanna, a1300, Shinya Sakae and 11 more@neodon I tried to reproduce your approach but I get a "not found" error ( I'm using Node 19 )
tsconfig.json
{ "compilerOptions": { "baseUrl": ".", "declaration": true, "esModuleInterop": true, "lib": ["es2022", "dom"], "module": "commonjs", "outDir": "build", "resolveJsonModule": true, "strict": true, "target": "es2022" }, "include": ["./**/*.ts"], "exclude": ["./build"] }package.json
{ "scripts": { "test": "tsc --noEmit && node --loader tsx --test test/**/*Tests.ts" }, "dependencies": { "@types/node": "18.11.9" }, "devDependencies": { "tsx": "3.11.0", "typescript": "4.8.4" } }./test/someTests.ts
import assert from 'assert/strict'; import { describe, it } from 'node:test'; describe('tests', () => { it('passes', () => { assert.ok(true); }); });
The test script fails with
Could not find '/home/.../repository/test/**/*Tests.ts'
Did I miss something?
Reacted by Brandon Bennett, official_dulin, Roberto Stagi, Nikolai Poriadin, DmPrkp, Sean Lennaerts, Quentin Laurent, Ofek Asido and Julian FortuneEDIT:
See this example repository which I have created.
Problem
I haven't tested your environment, but if I had to guess I believe it is your
npmscript which is incorrect:tsc --noEmit && node --loader tsx --test test/**/*Tests.tsOn a POSIX system
npmwill execute the script with/bin/sh. The patterntest/**/*Tests.tsis not expanded bynodeornpmit is up to/bin/shto expand it. However,**, also known asglobstaris not supported by/bin/sh, it is not being expanded as you would expect. Theglobstaris supported by/bin/bash, but must be enabled.Solution
I could be wrong, but I believe since
globstaris not supported, the patterntest/**/*Tests.tsbecomestest/*/*Tests.tswhich will only match files in a subdirectory of yourtestfolder, such astest/abc/xyzTests.ts. If you just wanted to match tests in the root of thetestfolder, you could rewrite the pattern astest/*Tests.ts, which would matchtests/xyzTests.ts, but would not match files in a subdirectory of yourtestfolder.Better Solution
It would be better to rewrite the script in a cross-platform manner, instead of relying on platform dependent features in your scripts which is may be unreliable. This way your script should even work on Windows.
There might be an easier way to leverage another package, but I think I would just move it into a JavaScript or TypeScript script.
Change your script to:
tsc --noEmit && node --loader tsx runTests.tsThen create a file named
runTests.ts.I don't have the time to experiment with a full script, but I expect you would want to use the
globpackage to get the list of files and thechild_processNode API to then callnode --loader tsx --test ....Related
Why you should always quote your globs in NPM scripts.
Follow Up
I originally wrote this answer of StackOverflow, but perhaps someone here has an easier solution than writing your own script. Also, I hope I've actually diagnosed the issue, otherwise that would be a little awkward 😬 .
Reacted by Neodon, Charles Riley, Brandon Bennett, Hassan, Alberto Esposito, Cristian Milá, Jarek Rencz, Noah Sherwin, Lee Junyeol, darcyrush and 13 moreAnother option to run the same command on Windows as well is to use globstar:
{ "scripts": { "test": "globstar -- node --loader tsx --test \"test/**/*.spec.ts\"" } }(Last release of this package seems to be 8 years ago, but still does the job.)
Reacted by Neodon, Scott Moore, Theryn Groetken, cedvdb, Javier Villegas, Juan Ignacio Dominguez, Noah Sherwin, ben, Gilliam, Matheus Torres and 7 more@scottwillmoore I was trying out your example and in Node v20, it now breaks with:
node-test-with-typescript on main [!] via 🤖 v20.0.0 on ☁️ production ❯ npm test > test > node --loader tsx --no-warnings script/test.ts /Users/ethanarrowood/Documents/github/vercel/vercel-azure-devops-extension/node-test-with-typescript/script/test.ts:1 import { execSync } from "node:child_process"; ^^^^^^ SyntaxError: Cannot use import statement outside a module at internalCompileFunction (node:internal/vm:73:18) at wrapSafe (node:internal/modules/cjs/loader:1187:20) at Module._compile (node:internal/modules/cjs/loader:1231:27) at Module._extensions..js (node:internal/modules/cjs/loader:1321:10) at Module.load (node:internal/modules/cjs/loader:1125:32) at Module._load (node:internal/modules/cjs/loader:965:12) at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:165:29) at ModuleJob.run (node:internal/modules/esm/module_job:192:25) Node.js v20.0.0The simplest solution I found was to add
"type": "module",to my project's package.jsonReacted by Scott Moore and StevenYou're right! This does not happen in Node 19 with
19.0.1, or even19.9.0, but it does happen in Node 20 with20.0.0. The error message appears to indicate that the TypeScript is not getting transpiled into CommonJS, but instead to ModuleJS. But, with no changes to the loadertsxand other dependencies, this must have been caused by a change in Node 20? However, I couldn't find any concrete reason in the changelog for Node 20.The solution, as indicated with @Ethan-Arrowood is to just add
"type": "module"to yourpackage.json, therefore Node will accept the TypeScript that is transpiled to ModuleJS. I've updated my example repository to reflect these changes. To be honest, if you are using the latest version of Node, you probably should use ModuleJS anyway.I've also double checked, and a custom script is still required to run TypeScript with the Node test runner at the moment. This is due to the current Node test runner execution model. However, there is a feature request to fix this issue: nodejs/node#46292.
Reacted by Daniel McNallyHmm when I try
node --loader tsx --testwith node 20 on windows.
I'm getting this error.node:internal/worker:211 this[kHandle] = new WorkerImpl(url, ^ This Environment was initialized without a V8::Inspector Thrown at: at Worker (node:internal/worker:211:21) at InternalWorker (node:internal/worker:464:5) at HooksProxy (node:internal/modules/esm/hooks:496:20) at CustomizedModuleLoader (node:internal/modules/esm/loader:351:18) at createModuleLoader (node:internal/modules/esm/loader:421:14) at get esmLoader (node:internal/process/esm_loader:15:26) at node:internal/modules/cjs/loader:119:47 at node:internal/util:764:15 at Module.load (node:internal/modules/cjs/loader:1128:26) at Module._load (node:internal/modules/cjs/loader:965:12)Hmm when I try
node --loader tsx --testwith node 20 on windows. I'm getting this error.node:internal/worker:211 this[kHandle] = new WorkerImpl(url, ^ This Environment was initialized without a V8::Inspector Thrown at: at Worker (node:internal/worker:211:21) at InternalWorker (node:internal/worker:464:5) at HooksProxy (node:internal/modules/esm/hooks:496:20) at CustomizedModuleLoader (node:internal/modules/esm/loader:351:18) at createModuleLoader (node:internal/modules/esm/loader:421:14) at get esmLoader (node:internal/process/esm_loader:15:26) at node:internal/modules/cjs/loader:119:47 at node:internal/util:764:15 at Module.load (node:internal/modules/cjs/loader:1128:26) at Module._load (node:internal/modules/cjs/loader:965:12)it seems to be caused by https://github.com/nodejs/node/blob/8becacb25d3c275341a9fef1a94581de3bd60c3d/src/env-inl.h#L638-L641
@nodejs/loaders @nodejs/workers do workers require a v8 inspector in order to work?
- Reacted by Scott MooreReacted by Moshe Atlow
A potential solution to the OP is to pre-compile TS to JS. Then you don't need a loader and with more than two unit tests it becomes a lot faster too, since you compile everything only once and no further overhead. (example commit that does this using swc).
Reacted by AndyI think it ended up being a red-herring.
Are you sure? I think it was a red herring for that PR in that Node’s internal test runner doesn’t use
--test, so the command you were using for one-off testing in that comment wasn’t working but the actual Node tests were passing. But I think maybe it really is a bug that--testand--loadercan’t be used together at the moment, because of https://github.com/nodejs/node/blob/8becacb25d3c275341a9fef1a94581de3bd60c3d/src/env-inl.h#L640. cc @MoLowReacted by Reinaldo CostaWeird. I have Windows 11 Pro (Build 22621) and in PowerShell with Node 20 installed with Volta I don't have any issues.
git clone git@github.com:scottwillmoore/node-test-with-typescript cd ./node-test-with-typescript volta --version # 1.1.1 node --version # v20.0.0 npm --version # 9.6.4 npm clean-install # Indirect npm run test # node --loader tsx --no-warnings ./scripts/test.ts # Direct node --loader tsx --no-warnings --test ./tests/add.test.ts
Could @MoLow try with this example, or make their example reproducible.
16 remaining items
@robertsLando
18.15specifically, some of the items like the compose() function on stream and test/reporters being accessible publicly were not available yet, mostly added in18.17I really tried but without any luck. Finally resorted to
run-tests.tsimport { run } from "node:test"; import { spec } from "node:test/reporters"; import process from "node:process"; import { glob } from "glob"; const tsTests = await glob("src/**/*.test.ts"); run({ files: tsTests }).compose(new spec()).pipe(process.stdout);
.swcrc{ "$schema": "https://json.schemastore.org/swcrc", "jsc": { "parser": { "syntax": "typescript", "tsx": false } } }and
package.json{ // ... "type": "module", "scripts": { // ... "test": "SWCRC=true node --loader @swc-node/register/esm run-tests.ts" // ... } // ... }Was it worth the effort? IDK.
Update:
- I'm using node v20.7.0
- @robertsLando's answer is better, I didn't see it. You can mix that and this to get a better result if you're already using
swc.
Reacted by Ryan VenegasAfter a lot of trial and error I got this to work on Windows:
"scripts": { "test": "glob -c \"node --test --require ts-node/register\" \"./tests/**/*.ts\"" },IMPORTANT: This command works only on linux
For run all test files in you project by "node" on typescript, run command bellow:
find -name "*test.ts" | xargs node --loader tsx --test
If it works, add it to scripts in
package.jsonI tried to run all tests with
node --loader tsx --test ./**/*.test.tsbut it isn't worked for me, it isnot tests nested test files, only selected layer.Reacted by James, Bernardo Rosa, Jason Howmans and Mirosław Filip@Dias1c pretty sure that's why people are using glob, it's cross platform :)
Reacted by Dias Kappassov and Scott MooreFor new Node versions. The
--loaderflag was deprecated in Node v20.6.0, use--importflag.This worked for me:
node --import tsx --test ./tests/*test.tsReacted by Scott Moore, Valdir Mendes, Caisesiume, Varun Sahni, Tim Heerwagen, Bosco Domingo, Dylan Lamont, South Drifter, declan, Mirosław Filip and 5 moreYou can just use
"test": "tsx --test src/*.test.ts"The real misunderstanding seems to be how default globbing works with nodejs. If you're using
zshfor example and runtsx --test src/**/*.test.tsIt will run fine. But if you use bash the shell it will fail. I assume the default globbing in nodejs uses the same backwards compatible syntax in linux where
*and**are equivalent.So to run all tests under
srcand all subfolders is not possible without either- Using a glob package that supports
**"test": "glob src/**/*.test.ts -c 'tsx --test'" - Using something like
findto find all the test files"test": "tsx --test $(find src -type f -name '*.test.ts')" - Manually including all possible nesting levels for folders
"test": "tsx --test src/*.test.ts src/*/*.test.ts src/*/*/*/*.test.ts"
Reacted by Helena Sathler, Andy, Kiril Knysh, Dias Kappassov, Aleksandr Sidorov, 40detectives, Jan Kowalski, Vasilii Chernov, Dylan Lamont, Bosco Domingo and 15 moreReacted by Helena Sathler, Anderson S Souza, Dylan Lamont, Paulo Sales, Daniel Kehlibarov, electrovir, Bas van Klaarbergen, Dmitry, mango and Jon Nyman- Using a glob package that supports
Hey guys, is anyone having issues with environment variables not being defined with node test runner? I am running my test with this command
node --experimental-test-coverage --no-warnings --test -r ts-node/register tests/api/base.test.ts
When I run this and try to console log process.env it's not defined. However, when I try to run a normal .ts script with commandnode -r ts-node/register testenv.tsand log process.env, it is able to log all environment variables.Reacted by Adrien JolyAwesome, thanks for the heads up. I've updated my example to use this approach. You can use the command below if you don't want to require Yarn.
"test": "glob -c \"node --loader tsx --no-warnings --test\" \"./tests/**/*.test.ts\""
Currently you can even shorten it to (on
Node.js 20.12.1)"test": "glob -c \"tsx --test\" \"./src/**/*.test.ts\""
Reacted by Bosco Domingo, Vladimir and Amir H. KhanjaniI just made a succinct guide to tie all of the learnings mentioned here together, including
.envfile and correct TS language server support in test files, all compatible with monorepos too!If you want a TL;DR, check this Gist instead.
Everything is free and I make no money from them either, it was just easier to share the info there for whomever may be interested :)
Node >=22.9.0 (if you want optionality for the
.envfiles)"test": "glob -c \"tsx --env-file-if-exists .env --env-file-if-exists .env.test --test --test-reporter spec \" \"./test/**/*.test.ts\"",
Node < 22.9.0 (throws error if the
.envfiles don't exist):"test": "glob -c \"tsx --env-file .env --env-file .env.test --test --test-reporter spec \" \"./test/**/*.test.ts\"",
node: "we now support typescript and built in tests"
also node: "but its not possible to run tests that are typescript files"Reacted by John Kaster, Bosco Domingo, Ryan Beall, an_dev, Petr Plenkov, Dunn, Justin Moser, Jackson Hall, Yony Calsin, Brian Zinn and 12 moreReacted by Léandre Daumont, Gianfranco P, cosmopetrich, salomovs95, Matías Hernán García, Alisson Vargas, Cody Ebberson, Maxime Richard, Mo, Imaduddin Haetami and 7 moreReacted by Aviv Keller, Yony Calsin, Imaduddin Haetami, Meotimdihia and Kenny Lavendernode: "we now support typescript and built in tests" also node: "but its not possible to run tests that are typescript files"
It absolutely is possible (unless I've misunderstood you):
#!/usr/bin/env -S tsx import test = require('node:test'); import setupTests = require('./setupTests'); type NodeTestRunnerParameters = Required<Parameters<typeof test.run>>; type NodeTestRunnerOptions = NodeTestRunnerParameters[0]; type NodeTestRunnerReturnType = ReturnType<typeof test.run>; const run = (proc: NodeJS.Process, options?: NodeTestRunnerOptions) => { proc.on('unhandledRejection', (error) => { throw error; }); // ... return test.run(options) } if (require.main === module) { const testStream = runTests(global.process, setupTests); }
// setupTests.ts import test = require('node:test'); import reporters = require('node:test/reporters'); import path = require('node:path'); import os = require('node:os'); type NodeTestRunnerParameters = Required<Parameters<typeof test.run>>; type NodeTestRunnerOptions = NodeTestRunnerParameters[0]; const options: NodeTestRunnerOptions = { // use either 'files' or 'testNamePatterns' - not both... files: [ path.resolve(path.join(__dirname, '/path/to/someModuleA.test.ts')), path.resolve(path.join(__dirname, '/path/to/someModuleB.test.ts')), ], // testNamePatterns: [ // "**/*.test.?(c|m)js", // "**/*-test.?(c|m)js", // "**/*_test.?(c|m)js", // "**/test-*.?(c|m)js", // "**/test.?(c|m)js", // "**/test/**/*.?(c|m)js" // ], concurrency: os.availableParallelism() - 1, setup(testsStream) { // Log test failures to console testsStream.on('test:fail', (testFail) => { console.error(testFail); process.exitCode = 1; // must be != 0, to avoid false positives in CI pipelines }); // coverage reporter const isTTY = process.stdout.isTTY; const reporter = isTTY ? reporters.spec : reporters.tap; testsStream.compose(reporter).pipe(process.stdout); }, // ... } satisfies NodeTestRunnerOptions;
chmod +x ./runTests.ts # then... ./runTests.tsor
yarn tsx ./runTests.ts
Apart from using tsx, there are other ways. But as it stands, tsx is the way to go for testing Typescript-based NodeJs projects, IMO.
I completely missed this conversation. This feature has been supported for a while.
Reacted by Moises Marquez and darcyrushSo I tried using the following on Node 22
node --require ts-node/register --test 'test/**/*.test.ts'",And while it successfully runs most of the time, it maxes out every core on my 32 core processor and total execution time is relatively slow. This means TTD is not an option without throttling my workstation. The other times at least one test fails with a segmentation fault.
It absolutely is possible
So I implemented the code above thinking it might improve things, but I have the exact same behavior. Even if I limit the parallelism to a few cores, they still max out.
Instead of using the code above or using ts-node as a loader, if I use the
tapepackage with ts-node directly, I can see that only five cores are used, at about 60% each and test execution is 33% faster. I still get the expected console output, but just with some extra empty tape output after...I don't have the time or expertise to dive into what exactly the issue is (if it's
ts-nodespecifically or something else) and I understand these features are in their early stages - I just wanted to note my experiences so far. Unfortunately, I will need to stick to using a third-party library as a test runner for now.Reacted by gregg-cbs
Details
I want to replace Mocha tests with the built-in testrunner. All tests are inside a test directory and follow the pattern
I started with a fooTests.ts file
and added the npm script
"test": "node --test ./test/**/*Tests.ts",The script fails with the message
How can I fix the script to
./test/**/*Tests.tsonly? Thanks in advance!
Node.js version
v18
Operating system
Linux ( Ubuntu 22.04 )
Scope
Built-in testrunner
Module and version
Not applicable.