Code Monkey home page Code Monkey logo

jest-light-runner's Introduction

jest-light-runner

A Jest runner that runs tests directly in bare Node.js, without virtualizing the environment.

Comparison with the default Jest runner

This approach is way faster than the default Jest runner (it more than doubled the speed of Babel's tests suite) and has complete support for the Node.js ESM implementation. However, it doesn't provide support for most of Jest's advanced features.

The lists below are not comprehensive: feel free to start a discussion regarding any other missing Jest feature!

Supported Jest features

  • Jest globals: expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach
  • Jest function mocks: jest.fn, jest.spyOn, jest.clearAllMocks, jest.resetAllMocks
  • Jest timer mocks: jest.useFakeTimers, jest.useRealTimers, jest.setSystemTime, jest.advanceTimersByTime
  • Inline and external snapshots
  • Jest cli options: --testNamePattern/-t, --maxWorkers, --runInBand
  • Jest config options: setupFiles, setupFilesAfterEnv, snapshotSerializers, maxWorkers, snapshotFormat, snapshotResolver

Unsupported Jest features

  • import/require mocks. You can use a custom mocking library such as esmock or proxyquire.
  • On-the-fly compilation (for example, with Babel or TypeScript). You can use a Node.js module loader, such as ts-node/esm.
  • Tests isolation. Jest runs every test file in its own global environment, meaning that modification to built-ins done in one test file don't affect other test files. This is not supported, but you can use the Node.js option --frozen-intrinsics to prevent such modifications.
  • import.meta.jest. You can use the jest global instead.

Partially supported features

  • process.chdir. This runner uses Node.js workers, that don't support process.chdir(). It provides a simple polyfill so that process.chdir() calls still affect the process.cwd() result, but they won't affect all the other Node.js API (such as fs.* or path.resolve).
  • Coverage reporting. This runner wires coverage data generated by babel-plugin-istanbul (Jest's default coverage provider). However, you have to manually run that plugin:
    • If you are already using Babel to compile your project and you are running Jest on the compiled files, you can add that plugin to your Babel configuration when compiling for the tests
    • If you are not using Babel yet, you can add a babel.config.json file to your project with these contents:
      {
        "plugins": ["babel-plugin-istanbul"]
      }
      and you can run Babel in Jest using a Node.js ESM loader such as babel-register-esm.

Usage

After installing jest and jest-light-runner, add it to your Jest config.

In package.json:

{
  "jest": {
    "runner": "jest-light-runner"
  }
}

or in jest.config.js:

module.exports = {
  runner: "jest-light-runner",
};

Using custom Node.js ESM loaders

You can specify custom ESM loaders using Node.js's --loader option. Jest's CLI doesn't allow providing Node.js-specific options, but you can do it by using the NODE_OPTIONS environment variable:

NODE_OPTIONS="--loader ts-node/esm" jest

Or, if you are using cross-env to be able to provide environment variables on multiple OSes:

cross-env NODE_OPTIONS="--loader ts-node/esm" jest

Don't run Node.js directly:

node --loader ts-node/esm ./node_modules/.bin/jest

This will result in ERR_UNKNOWN_FILE_EXTENSION, due to the loader argument not being passed to the sub-processes. This is a known limitation, and ts-node documentation recommends using NODE_OPTIONS.

Source maps support

If you are running transpiled code and you want to load their source maps to map errors to the original code, you can install the source-map-support package and add the following to your Jest configuration:

setupFiles: ["source-map-support/register"];

Stability

This project follows semver, and it's currently in the 0.x release line.

It is used to run tests in the babel/babel and prettier/prettier repositories, but there are no internal tests for the runner itself. I would gladly accept a pull requests adding a test infrastructure!

Donations

If you use this package and it has helped with your tests, please consider sponsoring me on GitHub! You can also donate to Jest on their OpenCollective page.

jest-light-runner's People

Contributors

2xic avatar alecmev avatar cjroebuck avatar fisker avatar iainjreid avatar janmeier avatar jlhwung avatar kripod avatar liuxingbaoyu avatar nicolo-ribaudo avatar piranna avatar simenb avatar swcm-mnestler avatar vajahath avatar zikoat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jest-light-runner's Issues

Document shared modules between tests

Hi, thanks for this awesome package; it has sped up my tests 10x, so I'm very happy with it!!

When I was first considering adopting jest-light-runner, though, one of the things that I most wanted to understand was exactly what type of test isolation I'd be giving up in exchange for the speed boost. I wasn't able to find much documentation on exactly how jest's test isolation works, so I mostly relied on the jest-light-runner README. After playing around with jest and this runner, though, I think this note in the readme is a bit incomplete:

Tests isolation. Jest runs every test file in its own global environment, meaning that modification to built-ins done in one test file don't affect other test files. This is not supported, but you can use the Node.js option --frozen-intrinsics to prevent such modifications.

In particular, from my experiments (please correct me if I'm wrong), it seems like Jest not only evaluates each test file it its own node VM, but also gives each of those VMs a distinct ESM cache (to use when linking modules into the VM), so that mutable ESM exports are also isolated (and re-evaluated) between test files. If that is correct, then it seems important to note that in the README, because --frozen-intrinsics obviously won't flag/prevent mutating module exports.

(As an aside, it might also be worth documenting that --frozen-intrinsics doesn't work with ts-node, I discovered, because the TS compiler sets some properties on the global Error object.)

Anyway, like I said, I think this library is awesome! I'm just opening this issue to double check that my understanding is correct, and, if so, note the confusing bits that I ran into when adopting this library (which did break a few of my tests at first), so that hopefully they can be clarified for future users.

jest.setSystemTime is not working, but mockdate is

Hi there, I'm trying to use the fake timers from jest, but I can't get them to work. I'm using jest-light-runner with esmock, and my dates are handled by dayjs.

Non-working snippet:

const now = dayjs();

beforeAll(async () => {
  await db.raw(`ATTACH DATABASE ":memory:" AS ${Customer.schema}`);
  await db.migrate.latest();
  jest.useFakeTimers();
  jest.setSystemTime(now);
});

Resulting in:

          OptIn {
    -       "effectiveAt": "2023-06-30T08:47:48.437Z",
    +       "effectiveAt": "2023-06-30T08:47:48.463Z",
            "name": "text",
            "value": true,
          },

Working snippet:

const now = dayjs();

beforeAll(async () => {
  await db.raw(`ATTACH DATABASE ":memory:" AS ${Customer.schema}`);
  await db.migrate.latest();
  MockDate.set(now);
});

Am I doing something wrong? I also tried legacy timers in jest to no avail.

jsdom environment

I'm guessing that this falls under "Unsupported Jest features", but I want to confirm:

Our project contains a mix of front-end (React) and generic (business logic) code, so we let Jest use its default node environment then configure individual test cases to use JSDom via @jest-environment jsdom docblocks. This is faster than initializing JSDom everywhere.

In a brief experiment with jest-light-runner, I saw that these tests are failing. I assume that, because jest-light-runner doesn't use vm to isolate tests, it doesn't support @jest-environment docblocks and must run every test in the same environment?

Thank you.

Using jest's fake timers completely freezes test execution

Using fake timers in multiple tests freezes the execution, with no other option but to kill the process.

import dayjs from "dayjs";

const now = dayjs();

beforeEach(async () => {
  jest.useFakeTimers().setSystemTime(now.toDate());
})

afterEach(async () => {
  jest.useRealTimers();
})

Calling jest.useRealTimers(); in afterEach as suggested here doesn't help unfortunately. Sorry I took so long go get back to creating a dedicated issue.

jest.setTimeout is not a function

I've been trying to migrate a module to using this test runner (thank you so much!).
Unfortunately I got stuck behind an error: TypeError: jest.setTimeout is not a function
I am not sure if this is a problem in this repo to be honest, but suspect it might not be supported here? If so, is there an alternative?
Thanks!

incompatibility: wrong nested beforeEach execution order

The following test passes with jest but not jest-light-runner. The beforeAll behaviour appears to be correct, though.

describe('beforeEach execution order', () => {
  const calls = []
  beforeEach(() => {
    calls.push('outer')
  })

  describe('nested', () => {
    beforeEach(() => {
      calls.push('inner')
    })

    it('expect outer beforeEach before inner', () => {
      expect(calls).toEqual(['outer', 'inner'])
    })
  })
})

Unclear how to integrate in typescript projects

I'm trying to use jest-light-runner on a testing project, and can't really understand the process I'm supposed to be following, if there is support for such a setup.

What I'm currently doing is adding the runner: "jest-light-runner" option to my jest config (which by itself will output errors for Unknown file extension ".ts").

I understand this is due to on-the-fly compilation not being supported, as mentioned by the docs. It is mentioned that I should use something like ts-node/esm, which I set up with the flag --loader ts-node/esm as part of NODE_OPTIONS.

Doing so is getting me random errors (from pino, our logger, not sure why). Is there anything else I should do? Does typescript support actually rely on this experimental node option, or is there anything else I'm missing?

beforeAll, beforeEach, afterEach, afterAll do not work from setupFilesAfterEnv

Hi there, thanks for the great project!

I consider jest's partial test isolation to be more trouble than it's worth and would rather switch to jest-light-runner, don't mess around with globals (expect with jest.spyOn) and use the built-in jest features to revert these changes after every test.

However, it seems that the resetMocks and restoreMocks config option no longer works with jest-light-runner (probably a separate bug), so I tried working around that using a custom setup script (with setupFilesAfterEnv).

Actual behavior

But it seems that beforeAll, beforeEach, afterEach, afterAll simply do nothing when called from such a script, for example:

setup-after-env.js:

console.log("setup-after-env")
beforeAll(() => console.log("setup beforeAll"))
beforeEach(() => console.log("setup beforeEach"))
afterEach(() => console.log("setup afterEach"))
afterAll(() => console.log("setup afterAll"))

demo.test.js:

it("should work", () => console.log("test"))

With jest-light-runner, this only prints:

setup-after-env
test

edit: Here's a minimal reproduction repo

Expected behavior

setup-after-env
setup beforeAll
setup beforeEach
test
setup afterEach
setup afterAll

Slower than Jest

I'm doing image snapshot testing, and it's very slow. The exact same file (no .concurrent) takes ~21 seconds with this runner, and only ~11 seconds with Jest's built-in. It has 58 snapshots, each invoking sharp to rasterize an SVG to PNG and passing it to jest-image-snapshot. Could I be missing something in my setup? I'm using ts-node/esm on top, just in case (and ts-jest for Jest).

Expand documentation on coverage report setup

Please expand the coverage report section of the README to include minimal steps to set up coverage reporting. Such that people with lesser knowledge of babel and loaders can follow.

Included discussion was based on my personal experience. For context I use pnpm with workspaces via rush.

Discussed in #71 (comment)

So I'v finally got it working.

  1. Install "babel-register-esm", save dev
  2. In ./jest.config.mjs
    collectCoverage: true,
    coverageProvider: "babel",
    coverageReporters: ["text"],
  3. Add file ./babel.config.json
    {
      "plugins": ["babel-plugin-istanbul"]
    }
  4. Run tests with NODE_OPTIONS='--loader=babel-register-esm' jest
    babal-istanbul

Importing files using a timestamp query parameter breaks with the `tsx` loader

I am experimenting with the tsx/@esbuild-kit/esm-loader together with jest-light-runner, and the inclusion of the current date when importing the test file results in an ERR_UNKNOWN_MODULE_FORMAT error (when --experimental-specifier-resolution=node is used, Node 18.16.1. Without this option I am running into ERR_MODULE_NOT_FOUND because of the .ts extension).

I came to realize this is because jest-light-runner loads test modules using an additional query parameter, which causes issues with the tsx loader. Note that the ts-node/esm loader does work, I presume they strip query parameters from the path before passing it through.

await import(pathToFileURL(testFile) + "?" + Date.now());

Removing the + "?" + Date.now() part avoids this problem, allowing the tests to run nicely (and fast, so fast! ๐Ÿš€) in runInBand mode (there's an unrelated issue when using workers, but I haven't looked into that sufficiently yet to tell what's going on).

It is not entirely clear to me what the date is trying to achieve. Is it for circumventing Node's module cache during watch mode? Should tsx perhaps identify the query param and strip it (which I assume is what ts-node does)?

Support .jest.clearAllMocks()

I'm not using require/import mock.

The following snippet works with mocking

// working
class Adder {
  constructor(public getValueToAdd: () => number) {}

  add(digit: number) {
    return this.getValueToAdd() + digit;
  }
}

test("testing mock", () => {
  const mockGetValueToAddFunction = jest.fn().mockReturnValue(1);
  const adder = new Adder(mockGetValueToAddFunction);

  const result = adder.add(2);

  expect(result).toBe(3);
  expect(mockGetValueToAddFunction).toHaveBeenCalled();
});

However adding

beforeEach(() => jest.clearAllMocks());

results in

Test suite failed to run

    TypeError: Cannot read properties of undefined (reading 'length')

      at file:/home/xxxx/node_modules/jest-light-runner/src/worker-runner.js:222:29
          at Array.filter (<anonymous>)
      at toTestResult (file:/home/xxxx/node_modules/jest-light-runner/src/worker-runner.js:222:8)
      at default (file:/home/xxxx/node_modules/jest-light-runner/src/worker-runner.js:78:10)
      at async /home/xxx/node_modules/piscina/dist/src/worker.js:141:26

Typescript support

Feature request: would be awesome if it supported typescript compilation (no type-checking needed).

Maybe something simple like esbuild transformer can be added for *.ts files, that'll cover 99% of the common needs.

Awesome runner, by the way!

Perhaps `jest.concurrent` can be supported?

In the babel repository, it takes 42 seconds to run all tests locally (12 workers), and 35 seconds for babel-cli alone.

Because of the large number of subprocess operations involved, serial is very slow.

Perhaps we can make testing faster by supporting concurrency, greatly improving our local development experience.

I'm willing to try to implement this feature (probably not soon) and open this issue for your opinion.๐Ÿ˜ƒ

Support source-maps

When tests fail in my typescript project then Jest shows JavaScript stacktraces which are not very helpful. This can be fixed by doing this in every test file:

import "source-map-support/register";

Maybe it would be a good idea to do this by default directly in jest-light-runner?

Repeated status logging due to ordering inconsistency

Thanks for this exciting project! I have been toying with the light runner a couple times now and it is hugely impressive, yielding up to two orders of magnitude performance improvements (going from ~400s to ~5s for ~2500 tests in ~400 suites, using --runInBand). Jest's isolation is hurting us a lot, even though we don't need it.

I am experiencing an issue where the start message that is sent over a test's MessageChannel may arrive after the tests' Promise has already resolved, i.e. the finished callback has already been executed. This results in excessive status message logging, as the late start event does activate the test as "running" even though it has already finished, rendering it in the "running" state forever (and consequently generating excessive status messages as more and more tests become considered "running").

I don't know exactly why/when this happens, nor do I know what the delivery guarantees are w.r.t. MessagePort message timing vs the micro task queue. My observations are with the InBandPiscina mode and this may be relevant for this to occur, considering that Piscina would otherwise communicate over message channels itself.

Potential solutions

I see two approaches that would fix my issue:

  1. Detect when a start message is received after the promise has resolved, then ignoring the start message entirely.
  2. Delaying the finish notification until after the start message has been received, then notifying the status runner of both events simultaneously (possibly with a microtick between the start and end notification, if needed)

I have tested locally with approach 1 and that seems to work just fine, but option 2 results in more consistently communicating test statuses without skipping status transitions (not sure if there's any invariants in this area).

I would be happy to contribute either fix, just opening this issue for discussion.

Support "setupFilesAfterEnv"

Custom matcher libraries like jest-extended suggest to load the extension by specifying setupFilesAfterEnv in the Jest configuration. This doesn't work with jest-light-runner, it is simply ignored so I have to load jest-extended in every test file manually. Maybe it would be possible to support the setupFilesAfterEnv configuration?

Tests isolation

Instead of using Workers, use cluster module ir child_process. This way, they would be full y independent processes so they would not share global atte, the same way Jest default environment does, and Aldo native chdir can be used.

"Your test suite must contain at least one test" with --runInBand and --watch

Hi, I'm getting the following error on subsequent test runs, (as in, the first run works fine, but any runs after making any changes give this error) when running with --runInBand and --watch:

    FAIL  src/__tests__/bar.js
  โ— Test suite failed to run

    Your test suite must contain at least one test.

      at onResult (node_modules/@jest/core/build/TestScheduler.js:172:18)
          at async Promise.all (index 0)

I made a mini reproduction repo here with instructions in the readme:

https://github.com/cjroebuck/jest-light-runner-runInBand-watch-bug-repro

I have tracked down the issue to this line https://github.com/nicolo-ribaudo/jest-light-runner/blob/main/src/worker-runner.js#L99

It seems that when run with --runInBand, the dynamic import when using InBandPiscina class is cached on subsequent test runs, and jest-circus fails to pick up the tests.

A hacky fix I made for this is to stick a random hash on the end of the import file path as a query parameter to 'bust' the import module cache, i.e.:

await import(pathToFileURL(testFile));

becomes

await import(pathToFileURL(testFile)+"?"+Date.now());

This works, but feels quite hacky and would probably lead to memory leaks if used in watch mode for many hours.

@jest/globals alternative

Switching to jest-light-runner with tests that use import {it, expect} from "@jest/globals" currently doesn't work. It's easily fixed by just removing this import and using those magically injected globals, but this way ESLint reports undefined variables and I lose my IDE's type features.

So could you either inject your own globals into from "@jest/globals" imports or expose your own globals, so I can use import {it, expect} from "jest-light-runner"?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.