Test runner: recursive or globbed loading of non-JS test files
meyfa opened this issue Β· 52 comments
What is the problem this feature will solve?
As per https://nodejs.org/dist/latest-v18.x/docs/api/test.html#test-runner-execution-model, the test runner can be started as node --test dirname/
to recursively run all test files (ending in .js
, .cjs
or .mjs
) contained in the directory dirname
. This unfortunately means that non-JS test files, specifically test files written in TypeScript in my case, will not be found since they end in .ts
instead of .js
. The docs say to provide each test file separately, e.g. node --loader=ts-node/esm --test dirname/foo.test.ts dirname/bar.test.ts
but this gets messy very quickly.
What is the feature you are proposing to solve the problem?
I see a few options (in order of my preference, starting with most preferred):
- Support globbing (e.g.,
node --loader ts-node/esm --test "dirname/**/*.ts"
). Note that this does work on Linux, to some extent, since the shell will expand asterisks. This doesn't work on Windows, though, and doesn't work when the string is quoted. Tools like mocha will perform globbing independently of the operating system or shell. - Provide a CLI arg for additional test file extensions (e.g.,
node --loader ts-node/esm --test --test-extensions=.ts,.cts,.mts dirname/
). - Have some sort of configuration file for the test runner where this can be configured.
- Let loaders tell Node about additional file extensions (e.g.,
node --loader ts-node/esm --test dirname/
and ts-node could hook into the directory traversal).
What alternatives have you considered?
- Listing test files manually (hard to maintain)
- Having a script do the globbing and construct the command line with all files explicitly listed
- Running
node --test
pointed to an index file that performs the globbing and dynamically imports all actual test files (very ugly; loses process separation between test files)
I also opened an issue at ts-node (TypeStrong/ts-node#1853) specifically for TypeScript, to see if they are interested in better test runner support.
@nodejs/test_runner
Honestly, I would just add .ts
support explicitly (and probably .tsx/.jsx
) given how common it is compared to the alternative with a helpful message in case people didn't pass a loader/require hook.
probably for starters, as @benjamingr support /.tsx?$/
as well,
eventually allow configuration of custom file filtering
I donβt think supporting ts (or jsx) by default makes any sense if TS/JSX content isnβt auto loaded.
Perhaps we should implement a solution for #43895 (that will allow passing a custom function to filter files)
I am willing to implement some --test-config
flag if there is consensus on it.
CC @nodejs/test_runner
Maybe a better solution would be to have an option for the loader to have an option to tell which file patten it supports for tests? /cc @nodejs/loaders
Maybe a better solution would be to have an option for the loader to have an option to tell which file patten it supports for tests?
why will it be better? I think a configuration file will cover more use-cases (real usecases from here), for example:
- within a file, filter which tests should run
- support glob (without relation to ts support)
loaders should process test files regardless of this discussion, there is a seperate issue about that
Running the test runner on a .ts
file only makes sense if there's a loader able to understand it, so it's convenient if the loader itself can provide this info. If the info comes from somewhere else, it's more likely to de-sync at some point. Anyway that was just a thought.
I completely agree that thatβs the only way to handle it. Nothing should be implicitly included unless node understands it;cams adding a loader should also implicitly include anything the loader handles. Users should only have to configure file extensions if theyβre deviating from their loadersβ defaults.
I might not fully understand how loaders work, so correct me in case I am wrong - a loader does not expose what file extensions it supports,
So there is no way for --test
to know what to extensions to filter
If we are talking about a new flag/option declaring what extensions a loader supports in the context of running tests - I don't really see a reason that should be part of loaders and not just part of the test runner config?
Personally, I see a huge advantage in letting users specify the file pattern precisely: I often find myself putting test fixtures, shared code etc. in the test/
directory (e.g. test/fixtures.ts
), along with test files (such as test/foo.test.ts
, test/bar.test.ts
). In these cases, I would want to use a glob like test/**/*.test.ts
instead of basing it off of the .ts
extension.
I see mts and cts file extensions were not mentioned above. This is evidence in favor of allowing users and customization hooks to specify.
I think probably just allowing an RE or glob of test files then as an argument for --test
?
I think probably just allowing an RE or glob of test files then as an argument for
--test
?
core doesn't currently support glob and regex can be hard to distinguish from paths
I think probably just allowing an RE or glob of test files then as an argument for
--test
?core doesn't currently support glob and regex can be hard to distinguish from paths
Currently --test
doesn't take any value right? So we could introduce a semver-minor change saying that now --test
can take an optional string value that will be interpreted as a regex I think.
Currently
--test
doesn't take any value right? So we could introduce a semver-minor change saying that now--test
can take an optional string value that will be interpreted as a regex I think.
it currently accepts paths
Currently
--test
doesn't take any value right? So we could introduce a semver-minor change saying that now--test
can take an optional string value that will be interpreted as a regex I think.it currently accepts paths
That's incorrect, --test
is a boolean flag:
Line 153 in d29e78a
Lines 529 to 531 in d29e78a
We could potentially change it to also accept a string. (i.e. node --test='/.\.test\.ts$/' path/to/test/dir
)
@aduh95 see
node/test/parallel/test-runner-cli.js
Line 11 in 2fe4e94
node/test/parallel/test-runner-cli.js
Line 23 in 2fe4e94
node/test/parallel/test-runner-cli.js
Line 55 in 2fe4e94
https://github.com/nodejs/node/blob/2fe4e9473fb50630326754886dee1d301a1a661e/test/parallel/test-runner-exit-code.js#L47
etc
I think a glob would an ideal solution:
- It is very common in test runners (I can't think of any test runner that does not support it), so users will already be familiar
- It addresses several other issues, such as using
spec
instead oftest
(egfoo.spec.js
)
If node does not understand the file it was explicitly pointed to, user-error. I can't imagine a user suddenly complaining that node didn't support typescript before and still doesn't when fed a glob on typescript. I think this will be even less of an issue when we add the more detailed error message to ESMLoader::resolve()
when an unknown file is encountered.
assuming that adding glob to core has a bigger scope than a small fix to the test runner,
what will be the process of adding this to core? which teams need to be CC'd and consulted before working on it?
and should some collaborator/tsc member contact user-land glob pacakge mainaners as a lesson learned from #43382?
Perhaps node:fs
?
import { glob } from 'node:fs/promises';
const [
file1,
file2,
...files
] = await glob('./src/**/*.spec.mjs');
As for the team, I imagine it depends on where it lives.
Yes, probably a good idea to reach out for lessons learnt π
I see that this was already raised here #40731 (comment) and #40954 (comment) with no real objections
@isaacs can you update where this stands? there is now a real use case for using minimach
internally,
I am willing to help with porting/implementing if help is needed and ok with you
Something else to consider: will node support a single flavor of globbing, or will it support multiple behaviors via options, similar to bash's various shopt
s: dotglob
, extglob
, etc? I'm guessing node --test <glob>
is interpreted in one and only one way, but the {glob} from 'fs'
API will support options?
Iβm growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner. Like are we going to vendor https://github.com/isaacs/node-glob or https://github.com/isaacs/minimatch now too, or create our own slimmed-down versions or alternatives? This feels a lot like the path we went down with chalk
in #43382.
Iβm growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner.
What dependencies has core already taken on because of the test runner?
Like are we going to vendor https://github.com/isaacs/node-glob or https://github.com/isaacs/minimatch now too, or create our own slimmed-down versions or alternatives?
It looks like that may already be in the works independent of the test runner. If they do make it into core, there is no reason for the test runner not to use them.
Iβm growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner.
I realize this might be a concern, and that is why I initially suggested introducing a configuration file for the test runner that will allow just importing glob or any other library to filter tests.
I still think will come in handy regardless of this specific issue
Like are we going to vendor isaacs/node-glob or isaacs/minimatch now too, or create our own slimmed-down versions or alternatives? This feels a lot like the path we went down with chalk in #43382.
IIUC, adding glob has been requested and raised a couple of times before the test runner existed, and the situation is very different than chalk
since the author of glob & minimatch is the one that has proposed adding it to core
introducing a configuration file for the test runner that will allow just importing glob or any other library to filter tests.
This is already possible without work from core, right?
require('node:child_process').spawn(
process.execPath,
['--test', ...require('glob').sync('./src/**/*.spec.mjs')],
{ stdio: 'inherit' }
).on('exit', process.exit);
It seems also quite easy to achieve that from an npm package, I'm not sure it's worth adding more to core when it's easy enough to implement in user-land.
This is already possible without work from core, right?
Possible yes. not great DX if you ask me :/
It looks like that may already be in the works independent of the test runner. If they do make it into core, there is no reason for the test runner not to use them.
Sorry, I wasnβt aware of this. This is great news; @isaacs maintaining their libraries as part of core seems like the ideal outcome.
These particular ones should definitely be in core regardless; but test runners are a quagmire of complexity, and choosing to ship one guarantees node will need a lot more new deps in the future - thatβs just the tradeoff that was chosen.
Or we could keep the test runner minimalistic, and let user land build on top of it to add more complex missing features.
That's a very interesting idea, but I suspect it would need a fundamental redesign/rearchitecting to be able to shoot for that, and a number of features that have landed and that are open PRs would need to be removed/rejected.
Or we could keep the test runner minimalistic, and let user land build on top of it to add more complex missing features.
I think this is theoretically a great idea. The "how" might be medicine worse than the disease though. E.G. a hook specifically to provide the glob results seems overkill: everyone is gonna want basically the same thing, so why have everyone do the same thing themselves.
I think it couldn't be simple json config (so couldn't go in package.json with the others planned to go in there); it would need to be a script file that returns config (the file path to which could go in package.json). Many test runners support similar script-to-config files, but then that begs the question: how is ours better. As a user, Mocha already has everything node:test
has as well as globbing yet isn't bloated like Jest, so if I have to do that crazy process.spawn etc, I'd just use Mocha.
if I have to do that crazy process.spawn etc, I'd just use Mocha.
Or you could use an npm package that reads a Mocha config and run it with Node.js built-in test runner. I agree that doing it in every projects would not make sense, but I think leaving it to the npm ecosystem is best for now, if there's a solution that indeeds seems to satisfy a great majority it can then be vendored in. (just my opinion, to be clear I'm not blocking the initiative)
Or you could use an npm package
are you saying the test runner --test
flag is not useful and only the node:test
module is?
My example (#44023 (comment)) is using the --test
flag, demonstrating its usefulness.
@MoLow what Antoine is saying is "we don't necessarily need config since we can parse whatever with a "regular" entry file and then pass it to child_process which would let people glob for example.
I (personally) think that glob
is common enough that users expect it to be part of core like regular expressions or dates like it is in Python/C#/other runtimes.
I also think that test config is common enough to be a file (or "known globals") though I wonder if a solution can't be either:
Running node:test
in "test mode" programmatically, essentially Antoine's suggestion with a (possibly) nicer API:
require('node:test').run({
concurrency: true,
timeout: 10000, // whatever
files: "**/*.test.ts"
});
Or alternative expose configuration through --require/--import
> node --import test-config.mjs --test
// test-config.mjs
import { config, beforeEach } from 'node:test';
config({
concurrency: true // or whatever
});
// add global setup/teardown code
beforeEach(() => assertNoLeaksOfDbHandles());
// configure stuff like test-double
It might be a good idea to set up a meeting where we each share testing experience and we write down the use cases we care about (like the one mentioned here of using it with a TypeScript project) and make sure our visions align.
@benjamingr that would be great - tbh that's what i'd have expected to happen before the runner landed in the first place :-)
Great, setting up a Doodle with some close dates (for early next week, some on the weekend, some in US friendly times and some in Europe friendly times).
it is intentionally close times since I believe these things work better when the meeting is close.
Also, @aduh95 can you please add me (and @cjihrig probably since he wrote the thing?) to the @nodejs/test_runner team?
(I think I can add myself since I'm an org-admin but I don't want to do that since I can only do that because I'm a moderator so I'd rather someone who is an org-owner because of the TSC do it)
Also maybe @isaacs since glob (minimatch) core inclusion will/may be discussed ?
We should also consider if an equivalent to this pseudo-code makes sense: require('node:test').runDashDashTest(['pathA', 'pathB'])
Equivalent to the behavior of the --test
flag. Understanding that yeah, --test
spawns child processes already. , but it reduces the total number by one.
@cspotcode yes that's a suggestion in #44023 (comment) :)
Also making sure you don't miss the meeting doodle at #44023 (comment)
Also, @aduh95 can you please add me (and @cjihrig probably since he wrote the thing?) to the @nodejs/test_runner team?
That's done!
I (personally) think that
glob
is common enough that users expect it to be part of core like regular expressions or dates like it is in Python/C#/other runtimes.
There have been discussions to include it in fs
(see #40731 (comment) and #40731 (comment)), so IMO a better API would be:
require('node:test').run({
concurrency: true,
timeout: 10_000, // whatever
files: require('node:fs').globSync("**/*.test.ts"),
});
IMO there should be a way to a file which name would be*.test.ts
in a directory named **
without the whole string being interpreted as a glob β of course that's a silly example, but I really think there's virtue in keeping such transforms explicit, same as we require file: URLs to be wrapped in new URL()
.
Other than that, I think that's a very neat idea, I like both options.
I am +1 on import { runTests } from 'node:test'
However, import { config } from 'node:test'
doesn't sound correct:
What happens if two different configurations are set in two different files?
What happens if it is configured once using --import
and then overridden inside some of the test files?
What happens if two different configurations are set in two different files?
What happens if it is configured once using--import
and then overridden inside some of the test files?
I assume config
would only apply when --test
is passed, so it would only have an effect if it's pre-imported (using --require
or --import
).
I assume
config
would only apply when--test
is passed, so it would only have an effect if it's pre-imported (using--require
or--import
).
that is pretty much what I suggested, but instead of using --test-config
it will use --import
?
One more ping to @nodejs/test_runner @cjihrig @aduh95 for the meeting doodle to make sure you saw it https://doodle.com/meeting/participate/id/dwjGRJmb
However,
import { config } from 'node:test'
doesn't sound correct:
What happens if two different configurations are set in two different files?
I feel like we should solve the overall question of how to configure Node from a config file, before solving specific use cases like node:test
and loaders. See also #43973.
@benjamingr is there a verdict for the meeting time?
Hey, due to a combination of bereavement leave and food poisoning / tummy flu I've totally dropped the ball on this and won't be able to engage in the next 3-4 days (at least until the Shiv'ah is after us).
I'll send another doodle next week (hopefully) or if someone else wants to pick it up - go tor it.