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

Allow to share global state between tests from globalSetup #7184

Open
dbartholomae opened this issue Oct 16, 2018 · 44 comments
Open

Allow to share global state between tests from globalSetup #7184

dbartholomae opened this issue Oct 16, 2018 · 44 comments

Comments

@dbartholomae
Copy link

🚀 Feature Proposal

Add a global property to the this of globalSetup and globalTeardown async functions that can be used to set global variables that can be accessed in all tests via global. The same global is shared across all tests.

Motivation

While jest was in the beginning used only for frontend testing, it has moved in the direction of becoming a general test framework. Especially for backend integration tests there is a tradeoff between test speed and departmentalization: Starting up a new backend instance for each individual test usually isn't feasible. Therefore most other test frameworks like mocha or jasmine provide possibilities to share state between tests, e. g. the backend instance. Usage examples include mocking http requests via nock.

Example

Let's assume an integration test that tests backend and database integration. The setup could look like this:

const backend = require('backend')

async function setupApp () {
  await new Promise((resolve, reject) => {
    backend.start().then((instance) => {
      this.global.backend = instance
    })
  })
}

module.exports = setupApp

And using the global could be done like this:

const request = require('supertest')
test('should call out to that fancy other api', () => {
  request(jest.globals.backend.url)
    .post('/some-endpoint')
    expect(200)
})

Pitch

As far as I know this change currently cannot be implemented outside of the main jest framework. Closest is an environment, but environments are sandboxed and do not share global state.

Open questions

How to best implement it?

I don't know the jest code well enough to have an idea how to best implement this. It might e. g. be easier to make the global available via global, or even jest.getGlobals().

Can we prevent misuse?

Sharing state between tests can lead to sideffects and random test breakage. One possible solution would be to make the jest.globals read-only, but I am not sure whether this is feasible without massively reducing which kind of objects can be stored.

@SimenB
Copy link
Member

SimenB commented Oct 16, 2018

First comments is that the setupfiles shouldn't assign to this. If we do this, I think the setup should return something, and we can assign that inside of jest itself (#5731 (comment)).

Also, I think anything assigned will need to be serializable, I don't think it's technically possible for it to be e.g. an instance of something (we need to send it to workers which is a separate node process)

@dbartholomae
Copy link
Author

I took the this pattern from environments. It seemed a bit odd to me, too, but would be consistent.

If the object needs to be serializable, then unfortunately a lot of merit of this feature would be lost. My main usecase would indeed be using nock which attaches itself to its processes http module and therefore needs to be called in the same process as the one where the backend is running. It would be possible to set up some helper though I guess that communicates via serializable data. In that case we are talking more about inter-worker-communication then mere globals.

@SimenB
Copy link
Member

SimenB commented Oct 16, 2018

Yeah, the envs are the ones that construct the global used in tests, so it makes sense that they have it. Doesn't mean it's a nice pattern, though 😀

Due to a hole in the sandbox (we give you the real core and native modules) nock should work. Note that it might break at any time, as that's a bug.

@dbartholomae
Copy link
Author

It should? Interesting, I'll give it another try. Last time I didn't get it to work. Basically what this feature proposal is about is providing a sanctioned way to do this.

@DanLambe
Copy link

I agree with @dbartholomae on this issue, I find it hard to recommend jest for all types of testing without the ability to share state between tests. I have a real usecase currently where the company I work for wanted to standardize our testing frameworks so I do to start using Jest over Mocha for my functional API testing for our react app. that was a mistake given that I have to fetch a new bearer token for every test file with no way of retaining that token to a variable "globally".

@thomasacook
Copy link

thomasacook commented Jan 15, 2019

I also agree with this issue - my team is using Jest/Supertest to test APIs for a microservice, and external service dependencies are faked using node/http. The setup is fantastic other than we have to use --runInBand always because we can't simply reuse external fake processes across tests, and it's not practical to test a single A microservice instance with various B and C dependency service instances running on random ports because each test is run in a separate process and can't access global node/http fakes of B and C. I hope this helps illustrate the issue at a high level better.

@jerimiah797
Copy link

My use case involves testing mobile devices with Appium. Without a global handle to the chromium webdriver (which connects to the device through the appium server and installs the app), each testfile must repeat this process of setup and teardown of the app. It adds up to 40 seconds for each testfile to go through this process. As it stands right now, I also have to --runInBand of course since otherwise the tests will all try to instantiate their own chromedriver connection at the same time.

I have seen some really gross workarounds to this problem that abstract the various tests in each testfile into regular js functions, and then make you call all the functions inside a single shell test.js file that contains the describe/it structure. I would really prefer not to do this since it breaks the ability to run a specific testfile on demand by passing the test as a CLI argument. :-(

@jasonworden
Copy link

jasonworden commented Mar 1, 2019

Fans of this may like the newly opened Feature Request as seen right above :)

Allow module sandbox to be disabled via configuration #8010

@Phoenixmatrix
Copy link

Just adding our use case:

We have an asynchronous initialization step that we need to do as a one time setup (and then expose the result to individual tests). The initialization is expensive and really should only happen once for the duration of the whole test run, but as it is, without runInBand, it's not possible.

@sheerun
Copy link

sheerun commented May 10, 2019

it would be very useful for us as well

@ianforsyth
Copy link

+1 here, there's some backend setup we'd like to share across all suites

@SimenB
Copy link
Member

SimenB commented May 13, 2019

Note that you'll never be able to share things that are not json-serializable as we have no way of passing that to workers. But sharing e.g. URLs to services will work whenever we get around to this.

So things like chromedriver connections talked about above cannot be supported. Puppeteer deals with this through exposing a websocket: https://github.com/smooth-code/jest-puppeteer/blob/master/packages/jest-environment-puppeteer/src/global.js

@emahuni
Copy link

emahuni commented May 13, 2019

This is a deal breaker, something has to be done for sure. Most of these apps take 20secs or more to bootstrap and having that happen for 30 or 50 times (once for each test file) is a big no no. It should only happen once as stated above. Can't Jest pass state to it's child processes or something along those lines. It'd be ok if all test files could just access even the master worker's global state.

@SimenB
Copy link
Member

SimenB commented May 14, 2019

No, that's not how communication between processes work: https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback

It's a bit better with worker_threads for builtin primitives, but not much: https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist


This isn't an API choice Jest has made, it's a fundamental technical limitation. Puppeteer allows connecting to a running instance through a websocket, you need to do something similar for whatever thing you're instantiating in a globalSetup.

@emahuni
Copy link

emahuni commented May 14, 2019

can you give a working example please? Whatever workaround you have to pass around instances, can't it be integrated into Jest or at least documented in Jest docs.

@SimenB
Copy link
Member

SimenB commented May 14, 2019

I don't know in how many ways I can say this, but I'll try one last time: you cannot pass around instances, it's not possible.

And seeing as this issue is still open, we have no solution for passing anything else either. jest-puppeteer adds the WS url to process.env, which works. When we fix this issue, they'll have an API they can use, which will be documented. We'll not be documenting hacks. The API will allow you to pass strings, numbers, objects and other primitives (not sure about regex, we'll see), but not instances

Please don't keep asking about things I've stated multiple times are not possible, or I'll have to lock this issue


can you give a working example please?

I recommend asking on StackOverflow or our discord channel for help.

@MrZhongFF
Copy link

take puppeteer for example:
https://jestjs.io/docs/en/puppeteer.html

module.exports = {
  globalSetup: './setup.js',
  globalTeardown: './teardown.js',
  testEnvironment: './puppeteer_environment.js',
};

@PhilipDavis
Copy link

@SimenB would it be feasible to not use worker processes, given some flag?

@ahnpnl
Copy link
Contributor

ahnpnl commented May 23, 2020

My use case is ts-jest wants to pass an Object of file content cache to global to allow each worker to access and use it

At the moment each worker performs readFileSync to access this file content cache from disk.

@mwallace72
Copy link

So I'm trying to understand - most of this discussion is how to handle non-serializable items, but where did we leave off on allowing serializable items from globalSetup?
I have a similar case with above where I am unable to set machine ENV variables on the fly through a script or other (whether during globalSetup, or outside it before running Jest). I would be attempting to store a serializable string as a global variable for my tests to achieve:

  • It is only initialized/stored once
  • It is available to all tests

There's not a good solution currently for Jest as the testEnvironment config file runs multiple times (for every test file), but is able modify variables to be made available to all tests; globalSetup config file runs once as desired, but is unable to modify variables to be made available to all tests.

@brianschardt
Copy link

I have the same use case as @adrianmcli any info?

@naruaway
Copy link

@adrianmcli @brianschardt
It looks like currently what we can do is something like the following if we want to avoid slow startServer() execution per test file

// entrypoint.test.js
beforeAll(async () => {
    global.server = await startServer();
});

import './tests/my-first-test';
import './tests/my-second-test';
import './tests/other-test';
// ./tests/my-first-test.js
test('my test', () => {
  expect(global.server.doSomeIntereaction()).toBe(...)
})

Of course, this does not come with parallelization.
Probably ideal solution would be launching startServer() as a separate process like jest-puppeteer does, but in that case I am not sure how we can easily swap mocked functions behind startServer() for each test case....

@airhorns
Copy link
Contributor

I think that #8708 would solve a bunch of problems people have that motivate this kind of state sharing. Instead of having to share something from the global setup down to the child processes, each of those child processes should (probably?) own one and only one of the resources. A puppeteer, a database connection, a booted server, etc, should be one per worker, not one global one per test run, and also not one per test file. That plays nicest with the automatic parallelization, keeps things fast, and I think is semantically sound with what Jest does already. Would that work for you folks and if so please thumbs up #8708!

@airhorns
Copy link
Contributor

airhorns commented Nov 20, 2020

Other resources like Kafka and ElasticSearch we use it in a multi-tenant fashion.
But to do so we must pass some information like connection string, admin credentials, unique run id for build parallelization on the same host etc.

Right, that's already possible with globalSetup via the process.env hack described above. You can boot the global resource once in globalSetup, and then pass whatever string-serialized state is necessary down to the workers via process.env. That's not training wheels, that's a fundamental limitation of the parallelization Jest gives you, so I'm not sure what you mean by that. If the workers need some piece of state that is the same for each worker but different between them, like say a Kafka connection or an ElasticSearch connection locked to a namespace for that worker, you still need to boot that up per-worker. If you want to have workers be different tenants then they need to act differently, no?

I think we can say there are many levels and contexts of setup:

  • per-invocation config (globalSetup, globalTeardown)
  • per-worker (not existent, see Per-worker setup/teardown #8708)
  • per-suite (setupFiles, setupFilesAfterEnv, beforeAll, afterAll)
  • per-test (beforeEach, afterEach).

There is a process boundary between the per-invocation and per-worker layers that allows for parallelization and the Jest authors have said won't go away. That's the right move IMO. That means you can't ever pass real objects and especially not real open sockets down from the one top places to N inner places.

What I am talking about is giving Jest users the ability to run setup once for the worker instead of once per suite. Connecting to ES or Redis or what have you once per suite isn't the end of the world, but for services that don't have multi-tenancy built in like puppeteer, or for app-land code that is expensive to boot, it'd be nice to do it once per worker instead of once per suite. Say creating kafka topics for the worker to use, or creating a Postgres database like mycoolapp_test__${process.env.JEST_WORKER_ID}. Right now that's not really possible, and I think it'd make managing these backend services a lot easier.

I also think tearing down Kafka / ES from jest isn't the best idea -- teardowns are more best effort than gauranteed IMO. You can't really guarantee that the teardown runs because a dev could SIGTERM the process, or it could OOM, or whatever really. The best use of those backend services would be re-entrant and automatically massage whatever state is in them into the clean state necessary for the test.

@debugpai
Copy link

For people that just want to pass primitives like strings from setup file to tests you can do that using environment variables

process.env.FOO = 'bar'

But yeah this is a serializable state.

@MarquonsDesCompetences-ThiB

Sharing global states is possible thanks to configuration's property testEnvironment
Just :

  • create your own class extending NodeEnvironment (documentation in link above includes an example)
  • in your overriding setup method, share the global values from 'global' to 'this.global'. Example :
async setup() {
   await super.setup();
   
   this.global.util = global.util;
}

And voilà !

MarquonsDesCompetences-ThiB pushed a commit to MarquonsDesCompetences-ThiB/JS-Util that referenced this issue Jan 29, 2021
Working :
Tests_Environment to be set in jest.config.json's testEnvironment property
Global can be shared implementing setup :
this.global.my_prop = global.my_prop;

More info :
jestjs/jest#7184 (comment)

Tests with an http server : TestsServer_Environment
Not working. Maybe using Worker ? But this way objects must be serializable
@ggirodda
Copy link

ggirodda commented Feb 18, 2021

@MarquonsDesCompetences-ThiB I didn't test it, but I think that the setup function is called for each test accordiing to the doc

Note: TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment.

In my case, I have two scripts to run tests

  • the test one, that executes all the tests required in the index.test.js test file
  • the test-only to execute test separately
// package.json
{
  // ...
  "scripts": {
    // ...
    "test": "jest --forceExit --detectOpenHandles ./tests/index.test.js",
    "test-only": "jest --forceExit --detectOpenHandles --runInBand"
  },
  "jest": {
    "testPathIgnorePatterns": [
      "/node_modules/",
      ".tmp",
      ".cache"
    ],
    "testEnvironment": "node",
    "setupFilesAfterEnv": [
      "./tests/helpers/setup.js"
    ],
    "globals": {
      "__APP__": undefined
    }
  }
  // ...
}

in my setup.js, I setup the global app, for every test, so when I run the yarn test it'll be setup only one time, because every test is required there

// tests/helpers/setup.js

beforeAll(async (done) => {
    // setup the app
  global.__APP__ = await setupMyApp();
  done();
});

afterAll(async (done) => {
  // teardown the app
  await teardownMyApp()
  global.__APP__ = undefined;
  done()
});
// tests/index.test.js

const glob = require('glob');
const path = require('path');

it('app is defined', () => {
  expect(global.__APP__).toBeDefined();
});

glob.sync('./tests/**/*.test.js').forEach(function (file) {
  require(path.resolve(file));
});

If I want to execute test suites in separate way, I use the test-only script, with the file I want to test as argument.

@ahouck
Copy link

ahouck commented Jul 10, 2021

@MarquonsDesCompetences-ThiB I didn't test it, but I think that the setup function is called for each test accordiing to the doc

Note: TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment.

In my case, I have two scripts to run tests

  • the test one, that executes all the tests required in the index.test.js test file
  • the test-only to execute test separately
// package.json
{
  // ...
  "scripts": {
    // ...
    "test": "jest --forceExit --detectOpenHandles ./tests/index.test.js",
    "test-only": "jest --forceExit --detectOpenHandles --runInBand"
  },
  "jest": {
    "testPathIgnorePatterns": [
      "/node_modules/",
      ".tmp",
      ".cache"
    ],
    "testEnvironment": "node",
    "setupFilesAfterEnv": [
      "./tests/helpers/setup.js"
    ],
    "globals": {
      "__APP__": undefined
    }
  }
  // ...
}

in my setup.js, I setup the global app, for every test, so when I run the yarn test it'll be setup only one time, because every test is required there

// tests/helpers/setup.js

beforeAll(async (done) => {
    // setup the app
  global.__APP__ = await setupMyApp();
  done();
});

afterAll(async (done) => {
  // teardown the app
  await teardownMyApp()
  global.__APP__ = undefined;
  done()
});
// tests/index.test.js

const glob = require('glob');
const path = require('path');

it('app is defined', () => {
  expect(global.__APP__).toBeDefined();
});

glob.sync('./tests/**/*.test.js').forEach(function (file) {
  require(path.resolve(file));
});

If I want to execute test suites in separate way, I use the test-only script, with the file I want to test as argument.

But with all tests running under one test suite, you lose out on the ability to scale any integration tests with more than one database. Now everything is lumped together instead of being completely isolated.

@akauppi
Copy link

akauppi commented Jul 16, 2021

Also, I think anything assigned will need to be serializable, I don't think it's technically possible for it to be e.g. an instance of something (we need to send it to workers which is a separate node process)

I would kindly ask also Date objects to be automatically serialized.

The use case is about sharing a read only dataset (1..10kB) with tests. I'm currently pondering between temporary files (yuck!) and environment variables. The size limits of env.vars keep me slightly concerned, and a standard mechanism provided by Jest would be preferred.

@sahilrajput03
Copy link

If somebody wants to try the expected behaviour please read this comment.

@felixhagspiel
Copy link

Sharing functions as global variables would be awesome for e2e-testing with prisma.io as well. Right now each test has to establish a new database connection and prisma prints a warning that there are too many connections, as mentioned here

@agreenspan
Copy link

I also am looking for this feature. We are trying to set up a singleton mongoMemoryServer instance (with individual dbs per test suite) but have not been able to get the mongo instance from globalSetup accessible in the environment setup.

@github-actions
Copy link

github-actions bot commented Aug 3, 2023

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.

@github-actions github-actions bot added the Stale label Aug 3, 2023
@Bessonov
Copy link

Bessonov commented Aug 3, 2023

Remove stale label

@github-actions github-actions bot removed the Stale label Aug 3, 2023
@IevgenRagulin
Copy link

IevgenRagulin commented Sep 9, 2023

Based on this thread, it appears that it should not be possible to use global variables in tests if those variables were set up in globalSetup. This weirdly works for me (with a caveat though). I'm seeing a behavior where I can set and access a variable (a db container instance) but the changes are 1-directional. Global variables set in globalSetup can be seen in the tests. However, if I attempt to modify those variables in the test, they won't be accessible in the globalTeardown. Could someone explain to me why this is happening by chance?

Based on this thread this isn't the behavior one should expect to see, so I worry that I can't rely on my setup continuing to work. I've tried this both with --runInBand and without with the same behavior. I've tried setting global context by using globalThis, process.env and a variable at the top level of a shared file like in the example below. In all scenarios I'm seeing this 1-directional writing allowed.

Here is some example code:

jestGlobalSetup.ts:

module.exports = async () => {
    process.env.TESTCONTAINERS_RYUK_PRIVILEGED = 'true';
    await startMysql();
}
...
mysql.ts:

let runningContainer: StartedMySqlContainer | null;

export async function startMysql() {
    if (!runningContainer) {
        runningContainer = await new MySqlContainer().start();
    }
}

export function deleteMysqlVar() {
    runningContainer = null;
}

export async function stopMysql() {
    if (runningContainer) {
        await runningContainer.stop();
    }
}
...
test.ts:
beforeAll(async ()=> {
  // I would love to be able to start a single global MySQL instance from the beforeAll hook (instead of globalSetup) so
  // that the DB is started only when it's needed. If I do this though, globalTeardown won't see the runningContainer
  // variable in mysql.ts
  // startMysql();
));

test("test things", async () {
  // this works
  runningContainer.executeQuery('SELECT DATABASE()');
  // this won't do anything strangely from the perspective of globalTeardown - it will still see the runningContainer variable
  deleteMysqlVar();
})

...
jest.config.js:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  globalSetup: "./test/jestGlobalSetup.ts",
  globalTeardown: "./test/jestGlobalTeardown.ts",
  maxWorkers: 2,
};

@tcurdt
Copy link

tcurdt commented Sep 9, 2023

We have this working:


beforeAll(async () => {
  await server.start()
})

afterAll(async () => {
  await server.stop()
})

describe('user', () => {

  beforeAll(async () => {
    // bring db into a certain stage
  })

  let token
  describe('login', () => {

    it('logs in with the correct password', async () => {

      ...

      token = res.body.user.token
    })
  })

  describe('users', () => {
    // use token

We make sure the db is running before starting the tests and just start/stop the server.

It's not particular great to have test dependencies if the runner isn't aware of them (as it will just error out where instead it should just not run tests) ... but surprisingly it's the work around we managed to live with for the past few years.

HTH

@headfire94
Copy link

headfire94 commented Oct 22, 2023

One of the reasons I want to see this feature implemented is that in my tests, I require a module that takes some time to initialize due to its side effects, as exemplified here (this is simple example but there are module that does intensive calculations to pre-calculate curves). When I run the application, this initialization is not a problem because the module initializes only once. However, in isolated test environments, requiring such a module consumes time in every test, even though it would be completely safe to share the initialization between them.

I found an article how to use jest env to share cached module https://www.petecorey.com/blog/2018/11/05/bending-jest-to-our-will-caching-modules-across-tests/, but it works only inside same test file, because env initialize per file

@adrian-85
Copy link

Jest is a great framework that does a lot of things really well. But this is a pretty big, even deal-breaking limitation for some teams. At the very least this limitation/feature should be clearly documented and visible before teams get too far into Jest. Maybe that's already the case, and I've just done a terrible job of reading the docs, but it wasn't something I was aware of.

I know this doesn't work for every case here, but for the auth token setup, if you check whether your token variable is already defined before re-execution, you would technically only run the token generation API requests once.

async ensureAuthenticated () {
    if (!this.authToken) {
      await this.authenticate()
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests