在Jest中模拟全局变量

112
在Jest中,是否有办法模拟全局对象,如navigator或Image*?我已经放弃了这个,将其留给一系列可模拟的实用方法。例如:
// Utils.js
export isOnline() {
    return navigator.onLine;
}

测试这个小函数很简单,但是有点繁琐且不确定。我可以完成其中75%的部分,但这已经是我的极限了:

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

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

另一方面,如果我可以接受这种间接性质,现在我可以通过这些实用工具访问navigator

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

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

        /* More implementation */            
    }
}

...并且可以像这样进行确定性测试...

// 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();
});

在我使用的所有测试框架中,Jest感觉是最完整的解决方案,但每次我编写笨拙的代码只是为了可测试性时,我觉得我的测试工具让我失望了。

这是唯一的解决方案还是我需要添加Rewire?

*不要嘲笑。 Image 对于ping远程网络资源非常好用。


请参考以下链接:https://dev59.com/cFgR5IYBdhLWcg3wAJL7 - Michael Freidgeim
6个回答

133

由于每个测试套件都运行在自己的环境中,因此您可以通过覆盖它们来模拟全局变量。所有全局变量都可以通过global命名空间访问:

global.navigator = {
  onLine: true
}

覆盖只对当前测试有影响,不会影响其他测试。这也是处理 Math.randomDate.now 的好方法。

请注意,通过对 jsdom 进行一些更改,可能需要像这样模拟全局变量:

Object.defineProperty(globalObject, key, { value, writable: true });

1
是的,你可以在那里设置东西。但也许window中存在的所有内容并不都存在于global中。这就是为什么我不使用global.navigator.onLine的原因,因为我不确定global中是否有一个navigator对象。 - Andreas Köberle
1
请注意,作为一般惯例,现在并不是所有的全局属性都可以被覆盖。有些属性具有不可写的特性,将忽略值更改尝试。 - Daniel Nalbach
23
这个“覆盖”仅影响您当前的测试,并不会影响其他人。这是否有记录? - JamesPlayer
3
@JamesPlayer,我可以明确确认,在一个测试中的重写将会影响到其他测试。至少在同一测试组中是如此。 - JoCa
@JoCa 感谢提醒。我已经更新了答案,使其更加精确。 - Andreas Köberle
显示剩余3条评论

41
这样做的正确方法是使用spyOn。这里的其他答案虽然可以工作,但没有考虑清理并且会污染全局范围。
// beforeAll
jest
  .spyOn(window, 'navigator', 'get')
  .mockImplementation(() => { ... })

// afterAll
jest.restoreAllMocks();

6
这使我收到了“Property navigator does not have access type get”的消息 - 这个问题应该适用于哪个版本的Jest? - Antoni4
1
尝试: jest.spyOn('window.navigator', 'get') - Igor Pejic

23

可能自回答撰写以来Jest已经发生了变化,但似乎它不会在测试后重置全局变量。请参见附加的测试用例。

https://repl.it/repls/DecentPlushDeals

据我所知,唯一解决这个问题的方法是使用afterEach()afterAll()来清理对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();
  })
});

3
我也遇到了同样的问题,即全局变量在每次测试运行后没有被重置。在 afterEach() 中调用 jest.clearAllMocks(); 可以解决这个问题。 - JiiB
在 Angular 中... 导入 { global } 自 '@angular/compiler/src/util' - danday74
4
由于测试可以并行运行,即使在afterEach()中调用jest.clearAllMocks()也可能失败。 - gamliela
global 在整个 describe 块运行完后会被重置吗?整个测试套件呢?还是永远不会? - jayarjo

10
如果有人需要模拟带有静态属性的全局对象,那么我的示例应该会有所帮助。
  beforeAll(() => {
    global.EventSource = jest.fn(() => ({
      readyState: 0,
      close: jest.fn()
    }))

    global.EventSource.CONNECTING = 0
    global.EventSource.OPEN = 1
    global.EventSource.CLOSED = 2
  })

5
如果您正在使用react-testing-library并且使用该库提供的cleanup方法,则它将在文件中的所有测试运行完成后删除该文件中进行的所有全局声明。然后,这不会继续影响任何其他测试的运行。
例如:
import { cleanup } from 'react-testing-library'

afterEach(cleanup)

global.getSelection = () => {

}

describe('test', () => {
  expect(true).toBeTruthy()
})

1
我相信这是 @testing-library/react v9.0 的默认行为,而 cleanup-after-each 功能在 v10.0 中被移除了 -- https://github.com/testing-library/react-testing-library/releases - rpearce
1
你说“一旦文件中的所有测试都运行”,但你使用 afterEach,这是矛盾的。 - DLight

2

如果您需要在window.navigator上分配重新分配属性的值,那么您需要:

  1. 声明一个非常量变量
  2. 从全局/窗口对象返回它
  3. 通过引用更改原始变量的值

这将防止在尝试重新分配window.navigator上的值时出现错误,因为这些大多是只读的。

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");
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接