This is a bit rambling, sorry about that. Feel free to edit this to clean it up and add more to it.
Jest currently has 51(!!) configuration options, many of which overlaps or intentionally overrides other options. (CLI options are out of scope for this issue, but they are obviously very much related)
They fall into a few different categories:
collectCoverageFrom
coveragePathIgnorePatterns
forceCoverageMatch
modulePathIgnorePatterns
testMatch
testPathIgnorePatterns
testRegex
transformIgnorePatterns
unmockedModulePathPatterns
watchPathIgnorePatterns
require
api (node paths, extensions)resetMocks
vs clearMocks
vs restoreMocks
automock
moduleDirectories
vs modulePaths
(I honestly have no idea)First of all, I'd like to simplify the file matching a lot. Both to make it easier to use, but also easier to implement and reason about.
For file matching I think coveragePatterns: Array<Glob>
, testPatterns: Array<Glob>
, transformPatterns: Array<Glob>
and remove everything else. You can use negated patterns to exclude things instead of a ignore
thing. No more force
to override ignores.
We could also group things (using the current names, although they are subject to change):
coverage
can have collectCoverage
, coverageDirectory
, coverageReporters
, coverageThresholds
, collectCoverageFrom
globalSetup
, setupTestFrameworkScriptFile
, setupFiles
, globalTeardown
.setup
which exported different functions. Not as declarative, but maybe better?module
can have automock
, resolver
, moduleDirectories
, modulePaths
, moduleNameMapper
, moduleFileExtensions
, resetModules
mocks
can have resetMocks
, clearMocks,
restoreMocks,
timers(
automock?`)snapshots
can have snapshotSerializers
, snapshotResolver
notify
and notifyMode
can be combined (true
is default mode, string
sets the mode)Another thing that's somewhat confusing is the difference between ProjectConfig
and GlobalConfig
. While the separation makes sense, it's invisible to users, and hard for them to reason about. It also doesn't work well with presets.
Finally, I leave you with this awesome article https://fishshell.com/docs/current/design.html 馃檪
Every configuration option in a program is a place where the program is too stupid to figure out for itself what the user really wants, and should be considered a failure of both the program and the programmer who implemented it.
I completely agree we should rethink our configuration options, so thank you for collecting all this.
Some comments:
For file matching I think coveragePatterns: Array
, testPatterns: Array , transformPatterns: Array and remove everything else. You can use negated patterns to exclude things instead of a ignore thing. No more force to override ignores.
I think we should favor regular expressions over globs, as they're more expressive and anything that can be expressed as a glob can be as a regexp. I'd also keep the ignore
versions as they're simpler than negated patterns.
We could also group things (using the current names, although they are subject to change):
I'm not sure we should group configuration options in objects. I've usually found flat options easier to deal with and it aligns better with CLI options (e.g.: you could overwrite the collectCoverageFrom
config option with a --collectCoverageFrom
CLI option).
I think we should favor regular expressions over globs, as they're more expressive and anything that can be expressed as a glob can be as a regexp.
The syntax is also way harder, though. I find globs more readable, and easier to write (as you can probably do ls myglob
in the terminal to test, that's hardet with regex). Escaping globs are easier as well, I think.
I'd also keep the ignore versions as they're simpler than negated patterns.
As long as we allow an array of globs and apply them in order, it's easy enough to just include an !
. Harder with regexes, though.
I'm not sure we should group configuration options in objects. I've usually found flat options easier to deal with and it aligns better with CLI options
I agree on this one, at least depending on how yargs could handle deep objects. If you can do coverage.pattern=blah
that's just as easy/readable imo
The syntax is also way harder, though. I find globs more readable, and easier to write (as you can probably do
ls myglob
in the terminal to test, that's hardet with regex). Escaping globs are easier as well, I think.
Also a fan of globs for file pattern matching. As long as you can pass arrays for all matchers, the limitations vs. regexp seem like not a big deal.
Another alternative (or addition) that would be great is just allowing any file pattern config to also accept (file: string) => boolean
as its argument and let the consumer do whatever they want.
Glob arrays + functions seems like a nice compromise of ease-of-use vs power to me.
Passing a function could definitely work. Would it be inline, or point to some file that exports a function? Both?
Edit: might be confusing that a string can be interpreted as a module, so I guess inline is the way to go
Passing a function could definitely work. Would it be inline, or point to some file the exports a function? Both?
It should be a function because otherwise it'd have to be split in two different configuration options (we can't distinguish a string being a glob or a file containing the function).
It should be a function because otherwise it'd have to be split in two different configuration options (we can't distinguish a string being a glob or a file containing the function).
Yes, this seem the most flexible since anyone is always free to just import their function.
Another related discussion here is that a some other config options require that something that's really just a function be implemented as an npm package, e.g. resolver
testResultsProcessor
, etc.
I'd prefer just allowing config to accept a function for these; people can always implement it as a module themselves if they want to. But it's a lot more cruft to make a module, link it in package.json
, etc for these things when I'd rather just import it directly where it's consumed.
It's something from when config had to be in package.json. I'm all for saying "use js config". Presets should help keep boilerplate down and if that's an issue for people.
We also have to consider people passing stuff from the CLI, but I don't think that _too_ important for stuff that rarely change (like result processors, resolver, etc).
The use case of setting certain reporters etc on CI only is easily solved in js config by inspecting the env (or using is-ci
)
I suppose you could overload again, and accept a string or an fn, where a string is always treated as an npm package? Specifying a reporter by npm package name on the CLI could be useful, though interestingly that can't be done now afaict ;)
You can do jest --reporters jest-junit
today
This is absolutely stupendous. Thank you all for your work here! It's long been a point of confusion for me.
Resuggesting the above linked issue here:
Ideally it would be very simple to configure Jest to use the same glob pattern that typescript uses to resolve paths.
Initially I assumed it had that type of support, but discovered it's regex only per this so question.
If this is supported users can use the same glob pattern in jest that they would use in typescript.
I don't know about the rest of you, but I break out into a cold sweat any time RegEx is mentioned.
As part of this, I'd also like to use cosmiconfig
to load configuration from files. It'll both allow us to delete code to look for a configuration file, and it'll allow us to support a few more ways to provide config (we currently have jest.config.js
, .jestrc
and jest
in packages.json
- all of which are supported by cosmiconfig)
Reposting here at request of @SimenB : https://github.com/facebook/jest/issues/7757
TL;DR: jest.restoreAllMocks()
-> jest.restoreAllSpies()
, and in config restoreMocks
-> restoreSpies
Would be nice to provide a way to specify reporter options (and possibly other things) from the CLI. See #7845
Edit: Sorry, out of scope 馃槃
From OP:
CLI options are out of scope for this issue, but they are obviously very much related
But yeah, we should figure out a plan. I think as long as we really standardize on how to pass options (I like the reporter:s [name, [name, options]]
pattern) it should be possible to figure out some relatively ergonomic way of doing it
Passing a function could definitely work. Would it be inline, or point to some file the exports a function? Both?
It should be a function because otherwise it'd have to be split in two different configuration options (we can't distinguish a string being a glob or a file containing the function).
One issue with passing functions is that they might encapsulate some state (usually in the form of a closure). E.g.
// jest.config.js
const myMatchingLibrary = require('my-matching-library');
return {
testMatch: [(filename) => myMatchingLibrary.isTest(filename)]
}
The only way we could use functions would be to serialize them to string (using .toString()
then new Function
or something on the other side), and that would lose the reference to myMatchingLibrary
.
So I think it would have to be a separate option like you mention.
E.g. either testMatch: Array<Glob>
_or_ testMatchModule: require.resolve('./my-file-with-matching')
. It would be a hard error to specify both.
my-file-with-matching
could use an external module, a regex etc.
After quite a lot a bunch of debates internally here's the current status:
Array<Glob>
type (and enable passing RegExp
in JS configs, but don't encourage it)--debug.detectLeaks
should always be supported.The new config format for the users would now look like this:
type NewConfig = {
coverage: {
enabled?: boolean;
paths: Array<Glob>;
reportDirectory?: string;
reporters: Array<string>;
threshold?: {
global: {
[key: string]: number;
};
};
};
debug: {
detectLeaks?: boolean;
detectOpenHandles?: boolean;
logHeapUsage?: boolean;
listTests?: boolean;
errorOnDeprecated?: boolean;
};
reporters?: {
json?: boolean | Path; // merge with "outputFile"
list?: Array<string | ReporterConfig>;
testLocationInResults?: boolean;
testResultsProcessor?: string | null | undefined;
useStderr?: boolean;
};
watch: {
paths: Array<Path>;
enable?: boolean;
all?: boolean; // former "watchAll"
plugins?: Array<string | [string, Record<string, any>]>;
};
// mocks
automock?: boolean;
unmockedModulePathPatterns?: Array<string>;
timers?: 'real' | 'fake';
clearMocks?: boolean;
resetMocks?: boolean;
resetModules?: boolean;
restoreMocks?: boolean;
// resolve
useBrowserField?: boolean; // former "browser"
dependencyExtractor?: string;
moduleDirectories?: Array<string>;
moduleFileExtensions?: Array<string>;
moduleNameMapper?: {
[key: string]: string;
};
modulePaths?: Array<string>;
resolver?: Path | null | undefined;
roots?: Array<Path>;
snapshotResolver?: Path;
cache?: Path | boolean; // merge wtih "cacheDirectory"
projects?: Array<Glob | Project>;
testMatch?: Array<Glob>;
name?: string;
displayName?: string;
globalSetup?: string | null | undefined;
globalTeardown?: string | null | undefined;
// test environment
sandboxInjectedGlobals?: Array<string>; // former "extraGlobals"
globals?: ConfigGlobals;
runtime?: Path; // former "moduleLoader"
setupFiles?: Array<Path>;
setupFilesAfterEnv?: Array<Path>;
snapshotSerializers?: Array<Path>;
testEnvironment?: string;
testEnvironmentOptions?: Record<string, any>; // remove "testURL"
testRunner?: string;
// test filtering
filter?: Path | boolean;
skipFilter?: boolean;
findRelatedTests?: Array<Path>;
runTestsByPath?: boolean;
testNamePattern?: string;
preset?: string | null | undefined;
runner?: string;
transform?: Array<{
paths: Array<Path>;
options: Record<string, any>;
transformer: string | Path;
}>;
maxConcurrency?: number;
// runner - global
updateSnapshot?: boolean;
bail?: boolean | number;
expand?: boolean; // show full diff
noStackTrace?: boolean;
passWithNoTests?: boolean;
notify?: NotifyMode;
prettierPath?: string | null | undefined;
replname?: string | null | undefined;
rootDir: Path;
forceExit?: boolean;
testFailureExitCode?: string | number;
watchman?: boolean;
haste?: HasteConfig & {skipNodeResolution?: boolean};
// vcs - global
changedSince: string; // remove "changedFilesWithAncestor"
onlyChanged: boolean;
lastCommit?: boolean;
};
We're still trying to make more sense out of all the config options, but we feel this is pretty close to what we would like to achieve
i'm not sure if this falls exactly into _simplifying configuration_, but requiring projects to install prettier to use inline snapshots seems like an extra complication to configuration. if the Jest API needs prettier to work, that's a direct (if optional) dependency of Jest, not projects using jest as a test runner.
On globs vs. regexp: I've been repeatedly confused by the patterns in test*
options, and so have others. Turned out we didn't know about micromatch
. One way to eliminate the confusion would be to have options names specifically with a RegExp
or Glob
suffix.
On simplifying configurations: any thoughts on cascading/extending/inheriting hierarchical config files? Here's a StackOverflow post requesting this feature.
I think ESLint does a great job at this with cascading configurations. It's been very easy to use in monorepos: have an .eslintrc
in the monorepo root with rules common to all projects, then in individual projects that need something different (e.g. env: {
browser: false,
node: true
}
vs. the other way around), only add those rules to a project-level .eslintrc
. I've :heart:ed this setup for a long while.
@dandv That also roughly echoes what Babel is moving towards, with one root babel.config.js
and then .babelrc
in folders that need configuration overrides.
Some random thoughts from a quick chat with @SimenB:
require.resolve
more things themselves? Would solve issues like overriding babel-jest
version without making it a peer dependency that has to be installed explicitly. Not good for JSON config, but perhaps users who use more complex config are likely to use JS config anyway.jest --transform $(node -p 'require.resolve("babel-jest")')
:stuck_out_tongue_closed_eyes: <rootDir>
a good thing in JS config where you already have path.resolve
etc like you use for e.g. webpack config? Probably wouldn't implement it today, instead just telling people to resolve it themselves, but we already have it and it works fine so idk. Plus it's still quite a bit shorter to write.Totally agree, configuration should be simpler and it saves us from wasting time to decide right option to use (while they're same :D) e.g. testRegex vs. testMatch in my last code review
Is there any reason to having both setupFiles and setupFilesAfterEnv? I cannot see any downside of only having setupFilesAfterEnv?
Is there any reason to having both setupFiles and setupFilesAfterEnv? I cannot see any downside of only having setupFilesAfterEnv?
I made a separate issue for this: https://github.com/facebook/jest/issues/9314
Curious, b/c I don't see it here (maybe I'm blind?) but does this include simplifying/clarifying the number of ways to make a mock or mock-like thing (automock, spyOn, jest.mock, __mocks__, requireMock, jest.fn)?
Hey there! No, this is for https://jestjs.io/docs/en/configuration and https://jestjs.io/docs/en/cli, not anything else. The features you mention are more documentation issues, I think.
Most helpful comment
This is absolutely stupendous. Thank you all for your work here! It's long been a point of confusion for me.