如何使用 Jest 模拟 JavaScript 窗口对象?

IT技术 javascript mocking jestjs
2021-02-04 05:40:11

我需要测试一个在浏览器中打开一个新标签的功能

openStatementsReport(contactIds) {
  window.open(`a_url_${contactIds}`);
}

我想模拟窗口的open函数,以便验证传递给open函数的URL 是否正确

使用 Jest,我不知道如何模拟window. 我尝试window.open使用模拟功能进行设置,但这种方式不起作用。下面是测试用例

it('correct url is called', () => {
  window.open = jest.fn();
  statementService.openStatementsReport(111);
  expect(window.open).toBeCalled();
});

但它给了我错误

expect(jest.fn())[.not].toBeCalled()

jest.fn() value must be a mock function or spy.
    Received:
      function: [Function anonymous]

我应该对测试用例做什么?

6个回答

以下方法对我有用。这种方法允许我测试一些应该在浏览器和 Node.js 中都可以运行的代码,因为它允许我windowundefined.

这是 Jest 24.8(我相信):

let windowSpy;

beforeEach(() => {
  windowSpy = jest.spyOn(window, "window", "get");
});

afterEach(() => {
  windowSpy.mockRestore();
});

it('should return https://example.com', () => {
  windowSpy.mockImplementation(() => ({
    location: {
      origin: "https://example.com"
    }
  }));

  expect(window.location.origin).toEqual("https://example.com");
});

it('should be undefined.', () => {
  windowSpy.mockImplementation(() => undefined);

  expect(window).toBeUndefined();
});
这应该是公认的答案,因为它模拟/间谍而不是更改实际的全局属性
2021-03-30 05:40:11
我刚刚使用了x = jest.spyOn(window, 'open')and x.mockImplementation(() => {}),仅供参考,但我需要模拟的只是 window.open。
2021-03-30 05:40:11
这比Object.defineProperty因为这允许在模拟时不影响其他测试要好得多
2021-04-02 05:40:11

而不是window使用global

it('correct url is called', () => {
  global.open = jest.fn();
  statementService.openStatementsReport(111);
  expect(global.open).toBeCalled();
});

你也可以试试

const open = jest.fn()
Object.defineProperty(window, 'open', open);
@AlexJM 你有同样的问题吗?介意分享你如何模拟窗口对象?
2021-03-23 05:40:11
谢谢!几个小时后,我只需要改变windowglobal
2021-03-25 05:40:11
我只是在我的测试中定义 window.property
2021-03-31 05:40:11
试过这个但对我不起作用。我的情况很奇怪,嘲笑在本地有效,但不适用于 Travis 中的 PR 合并......知道吗?
2021-04-04 05:40:11
@ Andreas 有什么方法可以将窗口模拟为未定义的stackoverflow.com/questions/59173156/...
2021-04-14 05:40:11

在 Jest 中有两种模拟全局变量的方法:

  1. 使用该mockImplementation方法(最类似于 Jest 的方法),但它仅适用于那些具有由jsdom. window.open是其中之一:

    test('it works', () => {
      // Setup
      const mockedOpen = jest.fn();
      // Without making a copy, you will have a circular dependency problem
      const originalWindow = { ...window };
      const windowSpy = jest.spyOn(global, "window", "get");
      windowSpy.mockImplementation(() => ({
        ...originalWindow, // In case you need other window properties to be in place
        open: mockedOpen
      }));
    
      // Tests
      statementService.openStatementsReport(111)
      expect(mockedOpen).toBeCalled();
    
      // Cleanup
      windowSpy.mockRestore();
    });
    
  2. 将值直接分配给全局属性。这是最直接的,但它可能会触发某些window变量的错误消息,例如window.href.

    test('it works', () => {
      // Setup
      const mockedOpen = jest.fn();
      const originalOpen = window.open;
      window.open = mockedOpen;
    
      // Tests
      statementService.openStatementsReport(111)
      expect(mockedOpen).toBeCalled();
    
      // Cleanup
      window.open = originalOpen;
    });
    
  3. 不要直接使用全局变量(需要一些重构)

    不是直接使用全局值,而是从另一个文件导入它可能更干净,因此使用 Jest 进行模拟将变得微不足道。

文件./test.js

jest.mock('./fileWithGlobalValueExported.js');
import { windowOpen } from './fileWithGlobalValueExported.js';
import { statementService } from './testedFile.js';

// Tests
test('it works', () => {
  statementService.openStatementsReport(111)
  expect(windowOpen).toBeCalled();
});

文件./fileWithGlobalValueExported.js

export const windowOpen = window.open;

文件./testedFile.js

import { windowOpen } from './fileWithGlobalValueExported.js';
export const statementService = {
  openStatementsReport(contactIds) {
    windowOpen(`a_url_${contactIds}`);
  }
}

我们也可以使用globalin定义它setupTests

// setupTests.js
global.open = jest.fn()

global在实际测试中调用它

// yourtest.test.js
it('correct url is called', () => {
    statementService.openStatementsReport(111);
    expect(global.open).toBeCalled();
});

在我的组件中,我需要访问window.location.search. 这是我在 Jest 测试中所做的:

Object.defineProperty(global, "window", {
  value: {
    location: {
      search: "test"
    }
  }
});

如果窗口属性在不同的测试中必须不同,我们可以将窗口模拟放入一个函数中,并使其可写,以便覆盖不同的测试:

function mockWindow(search, pathname) {
  Object.defineProperty(global, "window", {
    value: {
      location: {
        search,
        pathname
      }
    },
    writable: true
  });
}

并在每次测试后重置:

afterEach(() => {
  delete global.window.location;
});