jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
44.32k stars 6.47k forks source link

Jest projects do not inherit root/global config #10991

Open milesj opened 3 years ago

milesj commented 3 years ago

πŸ› Bug Report

This isn't mentioned anywhere in the docs, so I'm not entirely sure how this is supposed to work. But I have a monorepo (yarn workspaces) and I want some packages to have expanded Jest configs, while all of them to inherit "common" configs. I assumed that any settings in the root jest.config.js would be inherited for all project configs, but this does not seem to be the case, and is honestly very tedious to work around.

If I have this root config:

module.exports = {
  projects: ['packages/*'],
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};

I would expect all projects to inherit the setupFilesAfterEnv setting, but they do not. According to --showConfig, it's just an empty array. How else are we supposed to share config without having to create a jest.config.js in each project?

To Reproduce

Steps to reproduce the behavior:

Mentioned above.

Expected behavior

Projects would inherit the root/global config.

Link to repl or repo (highly encouraged)

envinfo

npx envinfo --preset jest

  System:
    OS: macOS 10.15.7
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Binaries:
    Node: 12.18.3 - ~/.nvm/versions/node/v12.18.3/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v12.18.3/bin/yarn
    npm: 6.14.6 - ~/.nvm/versions/node/v12.18.3/bin/npm
Rossh87 commented 3 years ago

Here's a link to a repo that verifies the behavior you're describing. However, this is almost definitely working as intended. If you'd like to share a partial configuration between projects, I believe this is the preferred approach.

That said, it wouldn't be overly complicated to search for a base config in the worktree root and merge it with each project config. Maybe there's interest in having a flag for this behavior?

manchuck commented 3 years ago

@Rossh87 seems strange that this is working as intended. I have a file that adds custom expect assertions that all projects are using. This means I have to copy over setupFilesAfterEnv into each project. Sure it is easy enough to spread it out when using jest.config.js however using package.json can lead to typos and make it annoying to change. There has to be some easier way to set this up

sigveio commented 3 years ago

The way I have it in my monorepos is using preset to point to a jest.preset.js in the root of the repo. Check out this example from Nx to see it in practise.

Rossh87 commented 3 years ago

@manchuck I probably shouldn't have tried to speak to the authors' intentions, since I'm not a maintainer. What I mean is just that I can imagine root config-sharing by default leading to confusion or slowing down tests a lot for monorepos whose projects have very different external dependencies (databases, servers, etc.) that need to be setup for tests. For example, if I have a client module and a server module in a monorepo that represents all the code for some website, I might want to spin up a real database for integration testing the server, but probably won't need it for testing the client. If the database setup ended up in the root config, I'd be needlessly spinning up a database every time I ran the client test suite, which isn't ideal. Conceptually, my impression is that workspaces are intended to be autonomous, and sharing setup creates implicit dependency, which isn't ideal IMO. I do see your point, though, and it's perfectly reasonable to disagree.

Depending on your specific case, you could try extracting your custom assertion library into its own workspace, turning it into a discrete package that your other workspaces can explicitly depend on by importing it. That's what I would personally prefer for a custom assertion library, but YMMV.

igorpupkinable commented 2 years ago

Here is an example of my Jest config for two cases: unit tests and integration tests. Unfortunately it is not possible to share repeated configuration in top level as it just does not work. It would help a lot if properties from top level would be augmented to projects with simple Object.assign.

const DO_NOT_CHANGE = 100;

module.exports = {
  collectCoverage: true,
  coverageReporters: [
    ['text', { skipFull: true }],
    'text-summary',
  ],
  projects: [
    {
      cacheDirectory: '<rootDir>/.jestcache',
      collectCoverageFrom: [
        'packages/*/src/**/*.ts?(x)',
      ],
      coveragePathIgnorePatterns: [
        '/__template__/',
        '/lib/',
        '/node_modules/',
      ],
      coverageThreshold: {
        global: {
          statements: DO_NOT_CHANGE,
          branches: DO_NOT_CHANGE,
          functions: DO_NOT_CHANGE,
          lines: DO_NOT_CHANGE,
        },
      },
      displayName: 'unit',
      errorOnDeprecated: true,
      globals: {
        'ts-jest': {
          tsconfig: '<rootDir>/tsconfig.common.json',
        },
      },
      moduleNameMapper: {
        '\\.css$': 'identity-obj-proxy',
        '^!svg-url-loader!(.*)$': '$1',
        '^!url-loader!(.*)$': '$1',
      },
      preset: 'ts-jest',
      resetMocks: true,
      setupFiles: [
        '<rootDir>/__tests__/setupTestEnvironment.ts',
      ],
      setupFilesAfterEnv: [
        '<rootDir>/__tests__/setupTests.ts',
      ],
      snapshotSerializers: [
        'enzyme-to-json/serializer',
      ],
      testMatch: [
        '**/src/**/*.spec.ts?(x)',
      ],
      transform: {
        '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileTransformer.js',
      },
    },
    {
      cacheDirectory: '<rootDir>/.jestcache',
      collectCoverageFrom: [
        'packages/*/src/?(*/)?(*/)*.ts?(x)', // Report only 2 levels deep.
        '!**/*.spec.ts?(x)',
      ],
      coveragePathIgnorePatterns: [
        '/__template__/',
        '/lib/',
        '/node_modules/',
      ],
      coverageThreshold: {
        global: {
          statements: DO_NOT_CHANGE,
          branches: DO_NOT_CHANGE,
          functions: DO_NOT_CHANGE,
          lines: DO_NOT_CHANGE,
        },
      },
      displayName: 'integration',
      errorOnDeprecated: true,
      globals: {
        'ts-jest': {
          tsconfig: '<rootDir>/tsconfig.common.json',
        },
      },
      moduleNameMapper: {
        '\\.css$': 'identity-obj-proxy',
        '^!svg-url-loader!(.+)$': '$1',
        '^!url-loader!(.*)$': '$1',
      },
      preset: 'ts-jest',
      resetMocks: true,
      setupFiles: [
        '<rootDir>/__tests__/setupTestEnvironment.ts',
      ],
      setupFilesAfterEnv: [
        '<rootDir>/__tests__/setupTests.ts',
      ],
      snapshotSerializers: [
        'enzyme-to-json/serializer',
      ],
      testMatch: [
        '**/__tests__/**/*.test.ts?(x)',
      ],
      transform: {
        '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileTransformer.js',
      },
    },
  ],
};
hesalx commented 2 years ago

@igorpupkinable Would you mind to elaborate on how exactly it does not work when sharing common properties using Object.assign ?

const DO_NOT_CHANGE = 100;

const common = {
  cacheDirectory: '<rootDir>/.jestcache',
  coveragePathIgnorePatterns: [
    '/__template__/',
    '/lib/',
    '/node_modules/',
  ],
  coverageThreshold: {
    global: {
      statements: DO_NOT_CHANGE,
      branches: DO_NOT_CHANGE,
      functions: DO_NOT_CHANGE,
      lines: DO_NOT_CHANGE,
    },
  },
  errorOnDeprecated: true,
  globals: {
    'ts-jest': {
      tsconfig: '<rootDir>/tsconfig.common.json',
    },
  },
  moduleNameMapper: {
    '\\.css$': 'identity-obj-proxy',
    '^!svg-url-loader!(.*)$': '$1',
    '^!url-loader!(.*)$': '$1',
  },
  preset: 'ts-jest',
  resetMocks: true,
  setupFiles: [
    '<rootDir>/__tests__/setupTestEnvironment.ts',
  ],
  setupFilesAfterEnv: [
    '<rootDir>/__tests__/setupTests.ts',
  ],
  snapshotSerializers: [
    'enzyme-to-json/serializer',
  ],
  transform: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileTransformer.js',
  },
};

module.exports = {
  collectCoverage: true,
  coverageReporters: [
    ['text', { skipFull: true }],
    'text-summary',
  ],
  projects: [
    Object.assign({}, common, {
      displayName: 'unit',
      collectCoverageFrom: [
        'packages/*/src/**/*.ts?(x)',
      ],
      testMatch: [
        '**/src/**/*.spec.ts?(x)',
      ],
    }),
    Object.assign({}, common, {
      displayName: 'integration',
      collectCoverageFrom: [
        'packages/*/src/?(*/)?(*/)*.ts?(x)', // Report only 2 levels deep.
        '!**/*.spec.ts?(x)',
      ],
      testMatch: [
        '**/__tests__/**/*.test.ts?(x)',
      ],
    }),
  ],
};
igorpupkinable commented 2 years ago

@hesalx you do augmentation manually in your example. I meant it should be done by Jest under the hood, e.g.

projects.forEach((project) => {
  Object.assign(project, { ..config, projects: undefined });
});
gsevla commented 2 years ago

It means root level jest config into a monorepo should only contain "projects" key? I assumed the same thought of @milesj in the beginning :/

A bit tricky in my opinion... as @manchuck said, it forces us to duplicate files with setup config.

An example: I'm using msw and I need to setup msw server using setupFilesAfterEnv. But, in order to do it, I need to setup my env file before using dotenv into setupFiles, so my server will be able to read my BASE_URL.

In this case I need to duplicate my setup files into every project/package of my monorepo.

manchuck commented 2 years ago

@gsevla I wound up just having the custom expectations in a file and mapping over all the projects ni the config, then appending the expectations. Hopefully that can help you keep the config DRY

ddbtrmatic commented 1 year ago

The documentation seems to indicate the root configuration should be copied to each project.

https://jestjs.io/docs/configuration#projects-arraystring--projectconfig

image

hesalx commented 1 year ago

Based on how it works in practice, I suppose the confusion comes from the usage of the term "root-level". Here "root-level configuration" means "the configuration file in the project's root" and not "the root of the exported object in the configuration file". This NOTE primarily clarifies the resolution of the template variables and not the inheritance of configurations per se. In other words, With the projects option enabled, Jest will copy each individual item of the projects option of the root-level configuration to respective child configuration during the test run, however the values will be resolved in the child's context, i.e. <rootDir> will point to the child's root directory and not to the root-level configuration directory.

mrazauskas commented 1 year ago

Perhaps clarity can be found in the discussion from the PR which added this note: https://github.com/facebook/jest/pull/12871.

github-actions[bot] commented 10 months ago

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

igorpupkinable commented 10 months ago

Bump