6

I'm stuck with a jest test setup. I want to test some client side code, an electron preload script, to be precise and want to use rewire to test methods that are not exported.

The script under test depends on a another script that accesses window.navigator. I can require the test script but it fails with a ReferenceError: navigator is not defined error on the first line in dom.js when I use rewire instead.

Here is a minimal setup that shows the error. I do not use a jest config as it defaults to the jsdom test enviroment.

dom.js

console.log('during rewire:', global.navigator)  // <-- prints undefined
console.log('The added property:', global.FOO)   // <-- prints undefined 

const userAgent = navigator.userAgent;           // <-- fails.

index.js

const myDom = require('./dom.js');

module.exports = {
  doSomething: () => {}
}

index.spec.js

const rewire = require('rewire');

describe('Basic test', () => {
  it('exports a function', () => {
    global.FOO = 'Bar'
    console.log('before rewire:', global.navigator)  // prints Navigator {}
    console.log('The added property:', global.FOO)   // prints Bar

    // const main = require('.')  // <-- works with require
    const main = rewire('.')      // <-- but fails with rewire
    expect(typeof main.doSomething).toBe('function');
  })
})

It looks as if rewire uses a different global, one that does not have the jsdom environment anymore (or not yet?) (like global.window.navigator or global.navigator).

Any help is highly appreciated, usually I find solutions for almost all problems on google but this time, I'm lost.

To reproduce, copy the three files to a folder, then do npm init, npm install jest rewire, npx jest.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
  • I guess `rewire` messes with `require`, while Jest already messes with it too. Why do you use it at all? Jest already has module mocking. – Estus Flask Sep 13 '20 at 11:40
  • Like mentioned, I want to test implementations of methods that are not exported. I also tried to dop in a manual mock for the `dom.js` but that didn't work either, rewire didn't use the mock but the real implementation. So if I find a solution for the problem here, then the rest should work. – Andreas Dolk Sep 13 '20 at 11:48
  • If it's your code, a good idea is to just export internal things for the sake of testability. I expect Rewire to be incompatible with Jest. Some other things like babel-plugin-rewire aren't, to my knowledge. – Estus Flask Sep 13 '20 at 12:03
  • IMHO, exporting methods or variables as well as changing the visibility of internal methods just for testing is never a good idea. Of course, we shouldn't unit-test internal methods either so in an ideal world, tools like rewire wouldn't be needed. But I still hope for a solution or explanation for that problem here... why should rewire use a different global object...? – Andreas Dolk Sep 13 '20 at 12:48
  • 1
    It's a good idea to export them if the alternative is hacking into internal mechanisms to achieve the same thing, like Rewire does. You can consider accessing them a reflection, which is a legit concept. Marking internals with a prefix (underscore or untypeable char) and JSDoc annotations (`@internal` is respected by Typescript) are common things. Not doing this is It may be possible to hack Rewire in some way to make compatible with Jest. But that they both are hacky is the real problem here, it needs to be addressed by opening an issue (likely Rewire, Jest issues are a dead end). – Estus Flask Sep 13 '20 at 13:18
  • 1
    Any way, the question is fine and deserves an answer. Possibly can be solved by hacking instead of fixing this in Jest or Rewire codebase. I don't remember how exactly both implement this, but my guess that the problem is two-fold, Jest uses Node `vm` sandboxing to run scripts and provides its own custom implementation instead of Node's own `require`, while Rewire doesn't correctly wrap around it. – Estus Flask Sep 13 '20 at 13:27
  • Found the place, where it happens, in rewire. rewire grabs the `require` function from the module and that function has the 'original' global in its scope, the one that does not contain my added value(s). So, new quest: find a way to replace the global object in the require methods scope :D (if possible) – Andreas Dolk Sep 13 '20 at 18:11
  • 1
    Here https://github.com/jhnns/rewire/blob/master/lib/moduleEnv.js#L50-L51 and here https://github.com/jhnns/rewire/blob/master/lib/moduleEnv.js#L50-L51 , Rewire uses `new Module(...).require` instead of Jest's enhanced `require` to require rewired module. A possible solution would probably be to prevent it from using Node's original `require` by patching `Module.prototype.require` from `module` module, but this should be done with jest.mock for `rewire` module only to not break other things. Not sure what the consequences would be. – Estus Flask Sep 13 '20 at 18:47
  • One thing is clear now: it does not work with the actual implementation of rewire. The `require` that is used during the rewiring has a different global scope then we see on the test. It misses `jsdom`, for example. I can change the implementation to use the `jest` require function - that uses the global that I want (with `jsdom`) but then all paths are relative to the rewire script - which I'd also have to change. At least I see a small chance for some sort of `jest-rewire` fork. – Andreas Dolk Sep 14 '20 at 06:05

0 Answers0