在 Jest 中模拟全局变量

IT技术 javascript unit-testing dependencies jestjs babel-jest
2021-02-02 11:48:46

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 感觉是最完整的解决方案,但是每当我编写笨拙的代码只是为了使其可测试时,我觉得我的测试工具让我失望。

这是唯一的解决方案还是我需要添加重新布线?

*别笑。Image非常适合 ping 远程网络资源。

6个回答

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

global.navigator = {
  onLine: true
}

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

请注意,通过jsdom 中的一些更改,您可能必须像这样模拟全局变量:

Object.defineProperty(globalObject, key, { value, writable: true });
“覆盖仅对您当前的测试有影响,不会影响其他测试。” - 在任何地方都有记录吗?
2021-03-19 11:48:46
请注意,作为一般做法,如今并非所有全局属性都可以覆盖。有些具有可写的 false 并且会忽略值更改尝试。
2021-04-02 11:48:46
是的,你可以在那里设置东西。但也许并非所有存在于中的东西window也存在于global. 这就是为什么我不使用global.navigator.onLine的原因我不知道,有一个navigator对象global
2021-04-05 11:48:46
@JamesPlayer 我可以肯定地确认,一个测试中的覆盖影响其他测试。至少在一个测试套件中。
2021-04-08 11:48:46
globalwindow在浏览器中一样吗?
2021-04-13 11:48:46

自从接受的答案写好后,Jest 可能已经改变,但 Jest 似乎并没有在测试后重置您的全局。请参阅随附的测试用例。

https://repl.it/repls/DecentPlushDeals

据我所知,解决此问题的唯一方法是使用afterEach()orafterAll()清理您对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();
  })
});
我遇到了同样的行为,每次测试运行后我的全局变量都没有重置。调用jest.clearAllMocks();afterEach()帮助我
2021-03-16 11:48:46
由于测试可以并行运行,甚至称jest.clearAllMocks()afterEach()可能会失败。
2021-03-20 11:48:46
在 Angular 中...从'@angular/compiler/src/util' 导入 { global }
2021-03-24 11:48:46

这样做的正确方法是使用spyOn. 这里的其他答案,即使它们有效,也不考虑清理和污染全局范围。

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

// afterAll
jest.restoreAllMocks();
尝试: jest.spyOn('window.navigator', 'get')
2021-04-10 11:48:46
这给了我“属性导航器没有访问类型获取” - 这应该是哪个版本的 Jest 工作?
2021-04-11 11:48:46

如果有人需要使用静态属性模拟全局那么我的示例应该会有所帮助:

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

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

如果您正在使用react-testing-library并且使用cleanup库提供方法,则一旦文件中的所有测试都运行,它将删除该文件中所做的所有全局声明。这将不会延续到任何其他测试运行。

例子:

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

afterEach(cleanup)

global.getSelection = () => {

}

describe('test', () => {
  expect(true).toBeTruthy()
})
您说“一旦文件中的所有测试都已运行”,但您使用的是afterEach,这是矛盾的
2021-04-06 11:48:46
我相信这是@testing-library/reactv9.0的默认行为,并且cleanup-after-each在 v10.0 中删除了功能 -- github.com/testing-library/react-testing-library/releases
2021-04-13 11:48:46