111

Is there any way in Jest to mock global objects, such as navigator, or Image*? I've pretty much given up on this, and left it up to a series of mockable utility methods. For example:

// Utils.js
export isOnline() {
    return navigator.onLine;
}

Testing this tiny function is simple, but crufty and not deterministic at all. I can get 75% of the way there, but this is about as far as I can go:

// Utils.test.js
it('knows if it is online', () => {
    const { isOnline } = require('path/to/Utils');

    expect(() => isOnline()).not.toThrow();
    expect(typeof isOnline()).toBe('boolean');
});

On the other hand, if I am okay with this indirection, I can now access navigator via these utilities:

// Foo.js
import { isOnline } from './Utils';

export default class Foo {
    doSomethingOnline() {
        if (!isOnline()) throw new Error('Not online');

        /* More implementation */            
    }
}

...and deterministically test like this...

// Foo.test.js
it('throws when offline', () => {
    const Utils = require('../services/Utils');
    Utils.isOnline = jest.fn(() => isOnline);

    const Foo = require('../path/to/Foo').default;
    let foo = new Foo();

    // User is offline -- should fail
    let isOnline = false;
    expect(() => foo.doSomethingOnline()).toThrow();

    // User is online -- should be okay
    isOnline = true;
    expect(() => foo.doSomethingOnline()).not.toThrow();
});

Out of all the testing frameworks I've used, Jest feels like the most complete solution, but any time I write awkward code just to make it testable, I feel like my testing tools are letting me down.

Is this the only solution or do I need to add Rewire?

*Don't smirk. Image is fantastic for pinging a remote network resource.

Andrew
  • 14,204
  • 15
  • 60
  • 104

6 Answers6

132

As every test suite run its own environment, you can mock globals by just overwriting them. All global variables can be accessed by the global namespace:

global.navigator = {
  onLine: true
}

The overwrite has only effects in your current test and will not effect others. This also a good way to handle Math.random or Date.now.

Note, that through some changes in jsdom it could be possible that you have to mock globals like this:

Object.defineProperty(globalObject, key, { value, writable: true });
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • Will `global` be the same as `window` in the browser? – Andrew Nov 07 '16 at 08:07
  • 1
    Yes in the the sense that you can set the stuff there. But maybe not all the stuff that is present in `window` is also present in `global`. Thats why I don't use `global.navigator.onLine` cause I'm not sure that there is a `navigator` object in `global`. – Andreas Köberle Nov 07 '16 at 08:27
  • 1
    Be aware that as a general practice not all global properties are overwritable nowadays. Some have writable false and will ignore value change attempts. – Daniel Nalbach Nov 06 '17 at 19:11
  • 22
    "The overwrite has only effects in your current test and will not effect others." - is that documented anywhere? – JamesPlayer Nov 12 '18 at 22:29
  • Great! I had to mock `performance`, which I'd never seen before, so I did: `global.performance = { now: () => {} };` – Brady Dowling May 30 '19 at 21:35
  • 3
    @JamesPlayer I can definitely confirm, that an overwrite in one test **will** effect the other tests. At least in one test suite. – JoCa Oct 10 '19 at 09:13
  • @JoCa Thank for mentioning it. I've updated the answer to be more precise. – Andreas Köberle Oct 10 '19 at 09:27
  • defineProperty worked but for the property I needed, it I had to do it this way. `Object.defineProperty(global, 'external', { writable: true }); global.external = { JSFunctionReady: JSFunctionReadyMock, };` Both in one call did not work. – Kenzie Revoyr Jun 22 '20 at 12:25
41

The correct way of doing this is to use spyOn. The other answers here, even though they work, don't consider cleanup and pollute the global scope.

// beforeAll
jest
  .spyOn(window, 'navigator', 'get')
  .mockImplementation(() => { ... })

// afterAll
jest.restoreAllMocks();
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jruz
  • 1,259
  • 1
  • 10
  • 6
23

Jest may have changed since the accepted answer was written, but Jest does not appear to reset your global after testing. Please see the testcases attached.

https://repl.it/repls/DecentPlushDeals

As far as I know, the only way around this is with an afterEach() or afterAll() to clean up your assignments to global.

let originalGlobal = global;
afterEach(() => {
  delete global.x;
})

describe('Scope 1', () => {
  it('should assign globals locally', () => {
    global.x = "tomato";
    expect(global.x).toBeTruthy()
  });  
});

describe('Scope 2', () => {
  it('should not remember globals in subsequent test cases', () => {
    expect(global.x).toBeFalsy();
  })
});
Barlas Apaydin
  • 7,233
  • 11
  • 55
  • 86
smeltedcode
  • 386
  • 3
  • 5
  • 3
    I experienced the same behaviour, that my global variable was not reset after each test run. Calling `jest.clearAllMocks();` in `afterEach()` helped me – JiiB May 23 '19 at 08:44
  • in Angular ... import { global } from '@angular/compiler/src/util' – danday74 Jan 29 '21 at 03:23
  • 4
    Since tests can run in parallel, even calling `jest.clearAllMocks()` in `afterEach()` may fail. – gamliela Sep 18 '21 at 05:20
  • Is `global` reset after whole `describe` block has finished running? Whole test suite? Or like never? – jayarjo Oct 14 '22 at 08:12
10

If someone needs to mock a global with static properties then my example should help:

  beforeAll(() => {
    global.EventSource = jest.fn(() => ({
      readyState: 0,
      close: jest.fn()
    }))

    global.EventSource.CONNECTING = 0
    global.EventSource.OPEN = 1
    global.EventSource.CLOSED = 2
  })
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kaspar Püüding
  • 182
  • 3
  • 10
5

If you are using react-testing-library and you use the cleanup method provided by the library, it will remove all global declarations made in that file once all tests in the file have run. This will then not carry over to any other tests run.

Example:

import { cleanup } from 'react-testing-library'

afterEach(cleanup)

global.getSelection = () => {

}

describe('test', () => {
  expect(true).toBeTruthy()
})
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    I believe this is the default behavior as of `@testing-library/react` v9.0, and the `cleanup-after-each` functionality was removed in v10.0 -- https://github.com/testing-library/react-testing-library/releases – rpearce Apr 08 '20 at 19:27
  • 1
    You says "once all tests in the file have run" but you use `afterEach`, which is contradictory – DLight Mar 03 '21 at 16:39
2

If you need to assign and reassign the value of a property on window.navigator then you'll need to:

  1. Declare a non-constant variable
  2. Return it from the global/window object
  3. Change the value of that original variable (by reference)

This will prevent errors when trying to reassign the value on window.navigator because these are mostly read-only.

let mockUserAgent = "";

beforeAll(() => {
  Object.defineProperty(global.navigator, "userAgent", {
    get() {
      return mockUserAgent;
    },
  });
});

it("returns the newly set attribute", () => {
  mockUserAgent = "secret-agent";
  expect(window.navigator.userAgent).toEqual("secret-agent");
});
Brady Dowling
  • 4,920
  • 3
  • 32
  • 62