Jest: Simplifying configuration

Created on 16 Oct 2018  路  27Comments  路  Source: facebook/jest

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:

  • file matching

    • some are arrays, some are not.

    • some are globs, some are regexes

    • collectCoverageFrom

    • coveragePathIgnorePatterns

    • forceCoverageMatch

    • modulePathIgnorePatterns

    • testMatch

    • testPathIgnorePatterns

    • testRegex

    • transformIgnorePatterns

    • unmockedModulePathPatterns

    • watchPathIgnorePatterns

  • modules and mocking

    • confusing overlap with node's require api (node paths, extensions)

    • resetMocks vs clearMocks vs restoreMocks

    • automock

    • moduleDirectories vs modulePaths (I honestly have no idea)

  • Coverage things

    • reporters

    • thresholds

    • output directory

  • Setup files

    • confusingly named, and inconsistent if they take array or string (#7119)

  • it keeps going (feel free to edit)

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
  • setup can have globalSetup, setupTestFrameworkScriptFile, setupFiles, globalTeardown.

    • @palmerj3 had a cool idea about having just a single 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.

Discussion

Most helpful comment

This is absolutely stupendous. Thank you all for your work here! It's long been a point of confusion for me.

All 27 comments

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:

  • move all "path" options to be Array<Glob> type (and enable passing RegExp in JS configs, but don't encourage it)
  • remove all "path ignore" options
  • unify "cache"/"cacheDirectory" and "json"/"outputFile"
  • rename "moduleLoader" to "runtime"
  • rename "browser" to useBrowserField"
  • rename "extraGlobals" to "sandboxInjectedGlobals"
  • remove "testURL" (can be set in "testEnvironmentOptions")
  • remove "changedFilesWithAncestor" (can be replaced with "changeSince=HEAD^)
  • remove "enabledTestsMap" (use "filter" instead)
  • extract "coverage" "debug" "reporters" and "watch" to groups
  • using grouped options from CLI is TBD and it may vary, but something like --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:

  • Should users 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.

    • Not so serious side note: For CLI usage, just jest --transform $(node -p 'require.resolve("babel-jest")') :stuck_out_tongue_closed_eyes:

  • Is <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.
  • Would be great to somehow find out what percentage of users use which configuration method (at least on public gh or something), but I haven't dug that deep into the advanced search queries yet :smile:

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

samzhang111 picture samzhang111  路  3Comments

gustavjf picture gustavjf  路  3Comments

jardakotesovec picture jardakotesovec  路  3Comments

Antho2407 picture Antho2407  路  3Comments

nsand picture nsand  路  3Comments