Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: first class Jest integration #1955

Closed
threepointone opened this issue Feb 9, 2021 · 56 comments
Closed

feature: first class Jest integration #1955

threepointone opened this issue Feb 9, 2021 · 56 comments
Labels
enhancement New feature or request

Comments

@threepointone
Copy link
Contributor

threepointone commented Feb 9, 2021

(previously...)

Making an issue to track work on the Jest experience with vite.

Jest is a very popular JavaScript testing framework. Some standout features:

  • Support for mocking functions and modules
  • Parallelisation and prioritisation of tests
  • Snapshot tests
  • Running only changed tests between 2 revisions (or based on a set of files)

It would be worthy to provide some form of blessed integration to use with vite.

(If you don't need these features, you may be better off using a testing framework like uvu, which should be much simpler to setup.)

@yyx990803 mentioned spinning up a ViteDevServer and using vite.transformRequest() as the transform. One problem here is that the jest transform is expected to be synchronous. Jest's likely going to fix this as part of the ES module work, tracking here. (Further, it's not clear to me what the devx would be? Should we run a vite server in a seaparate process/terminal tab? Or should we introduce a vite test command that wraps jest?


Let me propose a first step: in my own repo I'm going to do the bare minimum: esbuild-jest (possibly modified) + babel-plugin-transform-import-meta + babel-plugin-jest-hoist + babel-plugin-istanbul. Anything I'm missing? What are the vite specific bits to inject into the environment? Discuss!

@yyx990803
Copy link
Contributor

The main issue I see for not leveraging Vite itself for the transforms is that you won't get Vite plugin compatibility in Jest - e.g. if you add a Vite plugin that does some custom transforms, it won't apply to the Jest files. So this would still require Jest-specific transforms for e.g. Vue or Svelte files. I think your approach would work for J/TSX-only projects but not all Vite projects.

Another aspect of this is Vite configured alias and defines - they'd be automatically covered if the files are transformed by Vite, but you'd have to manually read the config and replicate them in your setup.

The ideal situation is to let Vite transform the files to how they'd run in Node (reusing part of the SSR transform logic), but Jest requiring transforms to be synchronous seems to be a major blocker here. Judging from jestjs/jest#9504 it seems there isn't much progress on this front :/

@yyx990803 yyx990803 added enhancement New feature or request discussion labels Feb 9, 2021
@threepointone
Copy link
Contributor Author

Yeah this has been running around my mind as well.

One idea I had, because it's all so fast, is to just do a build (without minification and concatenation) and have jest operate on that. 🤔

@threepointone
Copy link
Contributor Author

The PR for jest's async transform support jestjs/jest#9889

@threepointone
Copy link
Contributor Author

Ok I think I have ideas for hackzz. I'm going to try some stuff out, as and when I have time in the evenings. Will get back to this thread this weekend.

@nathanforce
Copy link

Yeah this has been running around my mind as well.

One idea I had, because it's all so fast, is to just do a build (without minification and concatenation) and have jest operate on that. 🤔

@threepointone I've been playing with this concept using esbuild for a little while. I'd be happy to compare notes if you'd like. Here's an example of what my tests look like.

That repo is a repro for a leak issue so it is stripped of anything non-essential, but the same pattern could be used in jest/ava/uvu.

@mpeyper
Copy link

mpeyper commented Feb 27, 2021

Just for anyone looking for something for right now, we wrote a (probably naive) Babel plugin that handles import.meta.env, import.meta.glob(...) and import.meta.globEager(...) transforms for our jest tests. I'm not sure what other custom transforms vite has, but this is what we needed for our project.

We have discussed extracting this into a stand alone package and publishing it if there is any interest in others using it.

@JacobMGEvans
Copy link

JacobMGEvans commented Feb 27, 2021

Just for anyone looking for something for right now, we wrote a (probably naive) Babel plugin that handles import.meta.env, import.meta.glob(...) and import.meta.globEager(...) transforms for our jest tests. I'm not sure what other custom transforms vite has, but this is what we needed for our project.

We have discussed extracting this into a stand-alone package and publishing it if there is any interest in others using it.

Was needed for enabling our Orgs OSS contributing to a Vite project since a majority of our group is familiar with Jest it seemed worth the extra effort!

It is now successfully running Jest + React Testing Library, locally and in GH actions

@mpeyper
Copy link

mpeyper commented Mar 11, 2021

Just for anyone looking for something for right now, we wrote a (probably naive) Babel plugin that handles import.meta.env, import.meta.glob(...) and import.meta.globEager(...) transforms for our jest tests. I'm not sure what other custom transforms vite has, but this is what we needed for our project.

We have discussed extracting this into a stand alone package and publishing it if there is any interest in others using it.

We ended up cleaning it up a bit and publishing it to NPM (source code).

@xiaoxiangmoe
Copy link
Contributor

@yyx990803 async code transformations are implemented by jestjs/jest#11191 and it was released in jest 27.0.0-next.5

@axe-me
Copy link
Contributor

axe-me commented Mar 31, 2021

I'm trying to write a vite jest preset recently. Now I got a blocker, that I need jest to allow async resolver to allow vite to do module resolving. see jestjs/jest#11226 (comment) and jestjs/jest#9505
Without a custom resolver, features like virtual ID, etc won't work.

@sodatea
Copy link
Member

sodatea commented Mar 31, 2021

FYI I've just implemented a super hacky vite-jest transformer with ViteDevServer.

The implementation
https://github.com/sodatea/vite-jest/tree/main/packages/vite-jest

A working example:
https://github.com/sodatea/vite-jest/tree/main/examples/vue-app-type-module

2 main hacks used in the implementation:

  1. Implemented a custom reporter to close the Vite server after test completion.
  2. As Jest does not support async resolver at the moment, I have to use a combination of moduleNameMapper to fix injected paths from Vite core plugins (adding aliases of /@vite/client and /@vite/env) and a simple regular expression replacement in the transformer implementation to support @fs URLs. transformIgnorePattern also needs to be fine-tuned.

And I have to patch Vite (https://github.com/sodatea/vite-jest/blob/main/examples/vue-app-type-module/patch-vite.js) to rename client.js & env.js to use the .mjs extension, so that they can be recognized by Node.js as ES modules.

@ianschmitz
Copy link

For those of us unfamiliar with Vite, why is it necessary to spin up the ViteDevServer to facilitate Jest testing? Requiring a recent version of Node and ESM support in Jest makes sense to me, but beyond those requirements how is it different from any other Node project (similar to #1149)? Is it that we're trying to avoid bringing in babel in favor of esbuild to keep consistency between dev/test env?

@sodatea
Copy link
Member

sodatea commented Apr 1, 2021

@ianschmitz

Yeah, it's for simplicity and consistency. Setting up Jest using esbuild-jest and other packages kinda works for now.

But like Evan has said earlier, to support other custom transformations, such as .svelte and .vue files, and import.meta. extensions (import.meta.env, import.meta.glob, etc.), Jest used to require additional transformers like vue-jest.

That's a lot of overcomplication.
We are basically duplicating all the transformation logic in Jest transformers again (and the configurations). And it's hard to make sure the behaviors are consistent between these implementations.

If we can spin up a ViteDevServer and make the Jest resolver/transformer simply proxy all the requests to that server, life would be so much easier.

@moljac024
Copy link

What's the status of this? Any updates? Vite is looking really great so far but the lack of first-class jest support is the only thing that's giving us pause on adopting it. I am interested in helping but I need a little bit of direction

@sodatea
Copy link
Member

sodatea commented Apr 29, 2021

@moljac024 Blocked by this issue: jestjs/jest#9505

@rememberlenny
Copy link

Is there anything I can do to push this forward?

@daaku
Copy link
Contributor

daaku commented May 26, 2021

Woohoo: Jest 27 shipped and includes async transforms!

@c10b10
Copy link

c10b10 commented Jun 4, 2021

Does this mean this can move forward?

@rememberlenny
Copy link

Is anyone working on a fix that we can follow?

@patak-dev
Copy link
Member

sodatea commented in Vite Land that jestjs/jest#9505 is still unresolved

@IanVS
Copy link
Contributor

IanVS commented Jun 8, 2021

I don't really know what I'm doing, but I really want jest support, so hold my beer: jestjs/jest#11540. (It's just a start, and has a long way to go before being done, but at least it's a start?)

@IanVS
Copy link
Contributor

IanVS commented Jun 9, 2021

@sodatea, after jest supports async resolvers, there will still be a few other issues that you mentioned in #1955 (comment), closing the server after tests (which seems fine to do in a reporter, I think), and renaming vite files from .js to .mjs. Do you have any thoughts on how we could avoid that one? Should vite build an .mjs version of the client, perhaps?

@zaverden
Copy link

Continuing #1955 (comment)

  1. Use babel with bare minimum of plugins, use embeded configuration
  2. ESM to CJS by @babel/plugin-transform-modules-commonjs
  3. Modules resulution by babel-plugin-module-resolver (with base support)
  4. import.meta.url by babel-plugin-transform-import-meta
  5. import.meta.hot and import.meta.env by custom babel plugin
  6. client.mjs and env.mjs from vite/dist/client (injected by vite when using import.meta.hot) are transformed too. I don't like this approach as it relies on user's jest config. stupid-simple alternative - transform them on transformer initialization, put into cache dir, and set /@vite/client and /@vite/env aliases to these transformed files

Well I think at this point it's time to move this code to the package and setup tests

@zaverden
Copy link

I tried my solution on react-ts and found that it is far from working state.

  1. vite replaces all libs imports by their wrappers from vite's cacheDir
  2. these wrappers import esm files from other packages
  3. vite dev server flushes wrappers to cacheDir with some delay, and it is possible to have a situation when jest want to resolve the module, but vite dev server hasn't flush it to the drive (Cannot find module '/vite-rts/node_modules/.vite/react.js' from 'src/react.test.ts').

need to figure out how to deal with it

@pionxzh
Copy link

pionxzh commented Oct 16, 2021

Just want to provide the jest config I'm using for people don't know how to run jest in vite project.
It works for Vue3 + Typescript.
Part of it is copied from vue-cli, and it doesn't use vite as bundler.

This way do not support import.meta.* feature, please use define in vite instead.

// https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-unit-jest/presets/default/jest-preset.js

module.exports = {
    preset: 'ts-jest',
    moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'vue'],
    // support the same @ -> src alias mapping in source code
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1',
    },
    transform: {
        '^.+\\.vue$': require.resolve('@vue/vue3-jest'),
        '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': require.resolve('jest-transform-stub'),
    },
    testMatch: [
        '<rootDir>/src/**/__tests__/**/*.{spec,test}.{js,jsx,ts,tsx}',
    ],
    watchPlugins: [
        require.resolve('jest-watch-typeahead/filename'),
        require.resolve('jest-watch-typeahead/testname'),
    ],
    // https://github.com/facebook/jest/issues/6766
    testURL: 'http://localhost/',
    globals: {
        // put your "define" variable here
    },
    testEnvironment: 'jest-environment-jsdom',
};

@marek-sed
Copy link

marek-sed commented Oct 26, 2021

Was porting our react snowpack project to vitejs, this was the biggest pain to get done, mainly because all react+ts examples are ignoring issue with import.meta. Which is really annoying

In snowpack this was solved, with create-snowpack-app which mimics cra setup. They use jest.config from package @snowpack/app-scripts-react .

there is nothing specific to snowpack there, so it can be easily used for vitejs or any other esbuild build tooling.
this is our current jest.config

// eslint-disable-next-line @typescript-eslint/no-var-requires
const snowpackConfig = require('@snowpack/app-scripts-react/jest.config.js')();

module.exports = {
  ...snowpackConfig,
  moduleNameMapper: {
    '\\.svg': '<rootDir>/src/__mocks__/svgrMock.js',
  '^@app(.*)$': '<rootDir>/src$1',
    '^test-utils$': '<rootDir>/src/utils/test-utils',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '^react-select/creatable/dist/react-select.esm$':
      '<rootDir>/node_modules/react-select/creatable',
    '^react-select/async/dist/react-select.esm$':
      '<rootDir>/node_modules/react-select/async',
  },
  modulePathIgnorePatterns: ['<rootDir>/src/locales'],
  transformIgnorePatterns: ['<rootDir>/node_modules/(?!d3-selection)/'],
};

and tests are running, love the project btw :).

@lloydjatkinson
Copy link

Is there any roadmap on implementing this feature? Right now, it's preventing a lot of people adopting or migrating to Vite. This is the case for React and TS, Vue, etc. The vite-jest package on npm simply does not work.

@IanVS
Copy link
Contributor

IanVS commented Nov 22, 2021

@lloydjatkinson as I understand it, jestjs/jest#9505 is one blocker on a more seamless vite integration with jest. I've got a PR opened in jest to add async resolver support, and the jest maintainers have said they plan to merge it, but it's been slow going so far.

@sodatea
Copy link
Member

sodatea commented Dec 7, 2021

https://twitter.com/haoqunjiang/status/1468053053651632128

So as of vite-jest v0.1.3, I think it's more than a Proof-of-Concept package now. (Example RealWorld PR at mutoe/vue3-realworld-example-app#82)

I need to find a Windows machine to test and fix the file path issue later.
But it's usable in *nix systems.

Just make sure you have understood the inherent limitation of this approach before trying it out: https://github.com/sodatea/vite-jest/tree/main/packages/vite-jest#limitations-and-differences-with-commonjs-tests

@mpblewitt
Copy link

For a quick fix for import.meta.env I kept using process.env so jest doesn't complain and used this vite plugin https://www.npmjs.com/package/vite-plugin-env-compatible

@fadi-george
Copy link

fadi-george commented Dec 8, 2021

Regarding import.meta, youd just need something like this for your babel config:

 function () {
    return {
      visitor: {
        MetaProperty(path) {
          path.replaceWithSourceString('process');
        },
      },
    };
  },

e.g.

module.exports = {
  env: {
    test: {
      plugins: [
        function () {
          return {
            visitor: {
              MetaProperty(path) {
                path.replaceWithSourceString('process');
              },
            },
          };
        },
      ],
      presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        '@babel/preset-typescript',
        [
          '@babel/preset-react',
          {
            runtime: 'automatic',
          },
        ],
      ],
    },
  },
};

@IanVS
Copy link
Contributor

IanVS commented Dec 10, 2021

While we wait for movement from jest to provide better support for projects like vite, I've created an example repo which shows how to use some of the best parts of jest (like its expect assertions and jest.fn() mocks) together with @web/test-runner to run unit tests in a headless browser: https://github.com/IanVS/vite-testing-example.

This approach has been working well for me so far, with 143 tests (including many react component tests) across 46 test suites running in three headless browsers (chromium, firefox, and safari) in ~35 seconds.

@charles-allen
Copy link

In the meantime, you might want to keep track of https://vitest.dev/

@BenjaBobs
Copy link

For those following this thread, https://github.com/vitest-dev/vitest just opened.

@mquandalle
Copy link

I think Vitest solves this issue?

@mazerty
Copy link

mazerty commented Jan 25, 2022

not really, but it is an interesting alternative indeed :)

@wandarkaf
Copy link

Maybe I can share my experience blending vite with jest. I hope everyone can find it helpful or at least insightful.

The only thing that worked for me was vite-jest, which is still a work in progress., as mentioned by @sodatea. But the approach works on macOS. Here's how to configure your project to run the Jest test successfully. In my case is more focused on Vue, but I think a react transition could be easy to make:

package.json

{
  // your configuration...
  "type": "module",
  "scripts": {
    // your scripts...
    "test:unit": "vite-jest --no-cache",
  },
  "devDependencies": {
    // your dev dependencies...
    "@vue/test-utils": "^2.0.0-rc.18",
    "jest": "^27.4.7",
    "jest-environment-jsdom": "^27.4.6",
    "vite-jest": "^0.1.4",
  }
}

jest.config.js

export default {
  preset: 'vite-jest',
  testMatch: [
    '**/tests/unit/**/*.spec.?(m)js?(x)',
    '**/__tests__/*.?(m)js?(x)',
  ],
  testEnvironment: 'jest-environment-jsdom',
}

vite.config.js

import { fileURLToPath } from 'url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

One thing to notice, and pretty important, is that your app will work on type: module mode. So it is good to know that we will need to tweak our project to work in that mode, i.e., no more require or module.export on your code, to mention a few.

I hope it helps those who want to work with Jest and Vite at the moment.

@owen26
Copy link

owen26 commented Feb 4, 2022

I might be able to share some of my experiences with Vite + React + Jest.

At the moment there is no perfect built-in solution indeed. And I'm hesitate to use a tool that is still considered WIP for our production project (e.g. vite-jest)

So what I did is basically write a jest config from scratch with a minimum 3rd party helper, in my case, this is the good old ts-jest.

The complete config file is straightforward.

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  moduleNameMapper: {
    '^~/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.svg$': '<rootDir>/__mocks__/svg-mock.js',
  },
  transform: {
    '^.+\\.(t|j)sx?$': ['ts-jest'],
  },
  setupFilesAfterEnv: ['./src/setupTests.ts'],
};

To solve the stylesheet importing issue, add module mapper and proxy them to identity-obj-proxy accordingly.

'\\.(css|less|scss|sass)$': 'identity-obj-proxy',

To solve SVG importing issue, add a mock file mapper

'\\.svg$': '<rootDir>/__mocks__/svg-mock.js',

then in the mapper do whatever fits your SVG transformation rules. In my case I love the create-react-app convention so what I need is

export default 'SvgrURL';
export const ReactComponent = 'div';

To solve the customized tsconfig path issue, simply do the same mapping to redirect them to the correct path.

'^~/(.*)$': '<rootDir>/src/$1',

To resolve import.meta issue, this is a bit tricky cuz I want to limit the usage of Babel under-the-hood for magical transformation, so instead, I decided to make my code better in a sense that those import.meta usage can be easily mocked.

I put all import.meta in a single file config.ts

const FOO = import.meta.env.FOO;
const BAR = import.meta.env.BAR;

export const APP_CONFIG = { FOO, BAR };

Then in Jest I just mock the config file via setupTest.ts, to avoid having to polyfill import.meta

jest.mock('~/config', () => {
  return { APP_CONFIG: { FOO: 'foo', BAR: 'bar' } };
});

@jensbodal
Copy link

jensbodal commented Mar 18, 2022

  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  moduleNameMapper: {
    '^~/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.svg$': '<rootDir>/__mocks__/svg-mock.js',
  },
  transform: {
    '^.+\\.(t|j)sx?$': ['ts-jest'],
  },
  setupFilesAfterEnv: ['./src/setupTests.ts'],

Shouldn't setupFilesAfterEnv: ['./src/setupTests.ts'] be setupFilesAfterEnv: './src/setupTests.ts'?

EDIT: I quoted the wrong one, I meant shouldn't '^.+\\.(t|j)sx?$': ['ts-jest'], be '^.+\\.(t|j)sx?$': 'ts-jest',?

@BenShelton
Copy link

  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  moduleNameMapper: {
    '^~/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.svg$': '<rootDir>/__mocks__/svg-mock.js',
  },
  transform: {
    '^.+\\.(t|j)sx?$': ['ts-jest'],
  },
  setupFilesAfterEnv: ['./src/setupTests.ts'],

Shouldn't setupFilesAfterEnv: ['./src/setupTests.ts'] be setupFilesAfterEnv: './src/setupTests.ts'?

No, it should be an array. See https://jestjs.io/docs/configuration#setupfilesafterenv-array

@jensbodal
Copy link

Sorry I commented on the wrong one, I meant

    '^.+\\.(t|j)sx?$': ['ts-jest'],

ts-jest should be a string

@patak-dev
Copy link
Member

I think we can close this issue now that vite-jest is available. Check the last comment from @sodatea here. I don't see further steps to be taken in Vite Core. Please create more focused issues in the proper repository (vite, jest, or vite-jest) moving forward.

As a note, Vitest is also now available as an alternative to Jest.

@jensbodal
Copy link

jensbodal commented Mar 19, 2022

I didn’t like the idea of having to spin up the vite server for testing. I’ve currently got this working with es-build so I guess I’ll just post my config for posterity’s sake.

jest.config.ts

import { get } from 'alias-hq';

import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
  displayName: 'web',
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  coveragePathIgnorePatterns: ['^.*/constants/.*$'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  moduleNameMapper: get('jest'),
  preset: '../../jest.preset.js',
  setupFiles: [
    '<rootDir>/scripts/test/setup-files-globals.ts', 
    '<rootDir>/scripts/test/setup-files-mocks.ts'
  ],
  setupFilesAfterEnv: ['<rootDir>/scripts/test/setup-files-after-env.ts'],
  transform: {
    '^.+\\.[tj]sx?$': 'esbuild-jest',
    '^.+\\.svg$': '<rootDir>/scripts/test/svg-transform.js',
  },
  transformIgnorePatterns: ['node_modules/(?!internalPackageThatExportsEsm|ky)'],
};

// eslint-disable-next-line import/no-default-export
export default config;

setup-files-globals.ts

import fetch, { Headers, Request, Response } from 'cross-fetch';
import React from 'react';

global.fetch = fetch;
global.React = React;
global.Headers = Headers;
global.Request = Request;
global.Response = Response;

svg-transform.js

module.exports = {
  process() {
    return 'module.exports = {};';
  },
  getCacheKey() {
    // The output is always the same.
    return 'svgTransform';
  },
};

setup-files-mocks.ts

const environment = {
  VITE_BUILD_COMMIT: 'mockBuildCommit',
};

jest.mock('../../src/constants/environment', () => environment);

setup-files-after-env.ts

import '@testing-library/jest-dom';

The mock for import.meta.env.VITE_BUILD_COMMIT only seemed necessary once I collated them to a constants file. But I’m fine with that setup.

@github-actions github-actions bot locked and limited conversation to collaborators Apr 3, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests