如何使用Jest模拟JavaScript中的'window'对象?

234

我需要测试一个在浏览器中打开新标签页的函数

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

我希望模拟 windowopen函数,以便验证正确的URL被传递给open函数。

使用Jest,我不知道如何模拟window。我尝试使用模拟函数设置window.open,但是这种方法不起作用。以下是测试案例:

it('the 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]

我该怎么处理这个测试用例?


请参见 https://dev59.com/5VYO5IYBdhLWcg3wI-Vw#9TEzoYgBc1ULPQZFwCd7。 - Michael Freidgeim
17个回答

208
以下方法对我有效。该方法允许我测试一些既可以在浏览器中运行,也可以在Node.js中运行的代码,因为它允许我将window设置为undefined
这是使用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();
});

6
这比 Object.defineProperty 好多了,因为这样可以在模拟时不影响其他测试。 - Sergey
5
这应该成为被接受的答案,因为它是通过嘲讽/间谍而不是更改实际的全局属性来实现的。 - user2490003
2
我刚刚使用了 x = jest.spyOn(window, 'open')x.mockImplementation(() => {}),顺便说一下,但我只需要模拟 window.open。 - temporary_user_name
3
很遗憾,这种方法与 TypeScript 不兼容。 - Brian C
@BrianC 你可以进行正确类型的转换。在这种情况下windowSpy = jest.spyOn(window, "window", "get") as jest.Mock; - undefined
显示剩余2条评论

131

使用 global 替代 window

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

您也可以尝试:

const open = jest.fn()
Object.defineProperty(window, 'open', open);

1
这些方法现在会抛出错误,但是 https://dev59.com/cFgR5IYBdhLWcg3wAJL7#56999581 仍然有效。 - marcw

37

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

  1. Use the mockImplementation approach (the most Jest-like way), but it will work only for those variables which has some default implementation provided by jsdom. window.open is one of them:

    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. Assign the value directly to the global property. It is the most straightforward, but it may trigger error messages for some window variables, e.g. 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. Don't use globals directly (requires a bit of refactoring)

    Instead of using the global value directly, it might be cleaner to import it from another file, so mocking will became trivial with 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}`);
  }
}

26

我直接将 jest.fn() 分配给 window.open

window.open = jest.fn()
// ...code
expect(window.open).toHaveBeenCalledTimes(1)
expect(window.open).toHaveBeenCalledWith('/new-tab','_blank')

18

在我的组件中,我需要访问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;
});

14

我们也可以在 setupTests 中使用 global 来定义它:

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

然后在实际的测试中使用global进行调用:

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

9
我找到了一个简单的做法:删除并替换。
describe('Test case', () => {
  const { open } = window;

  beforeAll(() => {
    // Delete the existing
    delete window.open;
    // Replace with the custom value
    window.open = jest.fn();
    // Works for `location` too, eg:
    // window.location = { origin: 'http://localhost:3100' };
  });

  afterAll(() => {
    // Restore original
    window.open = open;
  });

  it('correct url is called', () => {
    statementService.openStatementsReport(111);
    expect(window.open).toBeCalled(); // Happy happy, joy joy
  });
});

8

Jest中的window对象是自动模拟的

其他答案中未解决的问题之一是OP的评论:

使用Jest时,我不知道如何模拟window

window对象已经被自动模拟了,可以直接引用。

根据文档

Jest配合jsdom一起运行,它会模拟浏览器环境,因此每次调用的DOM API都可以像在浏览器中一样观察到!

示例:

describe('i am a window', () => {
    it('has a window object', () => {
      expect(window).toBeTruthy(); // test will pass
    });
});

5
你可以尝试这个方法:
import * as _Window from "jsdom/lib/jsdom/browser/Window";

window.open = jest.fn().mockImplementationOnce(() => {
    return new _Window({ parsingMode: "html" });
});

it("correct url is called", () => {
    statementService.openStatementsReport(111);
    expect(window.open).toHaveBeenCalled();
});

5

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