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

Posted

技术标签:

【中文标题】如何使用 Jest 模拟 JavaScript 窗口对象?【英文标题】:How can I mock the JavaScript window object using Jest? 【发布时间】:2017-06-12 15:43:17 【问题描述】:

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

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

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

使用 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]

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

【问题讨论】:

有没有计划将***.com/a/56999581/9888500 设置为接受的答案? 【参考方案1】:

以下方法对我有用。这种方法允许我测试一些在浏览器和 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();
);

【讨论】:

这比Object.defineProperty 好得多,因为这样在模拟时不会影响其他测试。 这应该是公认的答案,因为它模拟/间谍而不是更改实际的全局属性 我只使用了x = jest.spyOn(window, 'open')x.mockImplementation(() => ),仅供参考,但我只需要模拟window.open。【参考方案2】:

global代替window

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

【讨论】:

试过这个但对我不起作用。我的情况很奇怪,模拟在本地工作,但不适用于 Travis 中的 PR 合并......有什么想法吗? @AlexJM 你有同样的问题吗?介意分享一下你是如何模拟窗口对象的? 我只是在我的测试中定义了 window.property @Andreas 有没有办法将窗口模拟为 undefined ***.com/questions/59173156/… 谢谢!几个小时后,我只需要将window 更改为global【参考方案3】:

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

    使用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();
    );
    

    将值直接分配给全局属性。这是最直接的,但它可能会触发某些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;
    );
    

    不要直接使用全局变量(需要一些重构)

    与其直接使用全局值,不如从另一个文件中导入它可能更简洁,因此使用 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`);
  

【讨论】:

【参考方案4】:

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

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

并在实际测试中使用global调用它:

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

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

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

【讨论】:

谢谢这是最好的方法! 干净简洁!太棒了!【参考方案7】:

我找到了一个简单的方法:删除和替换

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】:

你可以试试这个:

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

【讨论】:

【参考方案9】:

如果类似window.location.href can't be changed in tests. #890的窗口位置问题,你可以试试(调整):

delete global.window.open;
global.window = Object.create(window);
global.window.open = jest.fn();

【讨论】:

【参考方案10】:

在您的 Jest 配置中,添加 setupFilesAfterEnv: ["./setupTests.js"],创建该文件,并在测试之前添加您要运行的代码:

// setupTests.js
window.crypto = 
   .....
;

参考:setupFilesAfterEnv [array]

【讨论】:

【参考方案11】:

可以测试一下:

describe('TableItem Components', () => 
    let open_url = ""
    const  open  = window;
    beforeAll(() => 
        delete window.open;
        window.open = (url) =>  open_url = url ;
    );
    afterAll(() => 
        window.open = open;
    );
    test('string type', async () => 
        wrapper.vm.openNewTab('http://example.com')
        expect(open_url).toBe('http://example.com')
    )
)

【讨论】:

【参考方案12】:

我有一个实用函数,它允许我像这样模拟窗口上的任何方法:

  function givenMockWindowMethods(methods: Partial< [key in keyof Window]: jest.Mock<any, any> >): () => void 
    const mocks = Object.values(methods);

    Object.entries(methods).forEach(([key, value]) => 
      Object.defineProperty(window, key,  value );
    );

    return (): void => mocks.forEach((mock) => mock?.mockClear());
  

因此,如果我需要在窗口上模拟 open 方法(或其他任何东西),我可以这样做:

      const cleanupMocks = givenMockWindowMethods( open: jest.fn() );
      // expect(...).toBe(...)

      //at the end of the test, clean it up
      cleanupMocks()

【讨论】:

【参考方案13】:

简单试试

let windowOpenSpy: jest.SpyInstance;
beforeEach(() => 
    windowOpenSpy = jest.spyOn(window, 'open');
);

it('should open window with dashboard url', () => 
    expect(windowOpenSpy).toBeCalledWith('your url', '_blank');
);

【讨论】:

【参考方案14】:
const windowSpy = jest.spyOn(iFrame, "contentWindow", "get");
windowSpy.mockImplementation(() => (
  location: 
    origin: "https://test.com",
    href: "href",
    hash: "hash"
  
));

【讨论】:

【参考方案15】:

我尝试了一个类似的测试并和我一起工作......

我的代码:

export const Blah = () => 
     const BLAH = 'https://www.google.com/'
     const handleBlah = () => 
        window.open(BLAH, '_blank')
      

  return (
     <button onClick=handleBlah> BLAHBLAH </button>

  )

我使用 Jest 进行的测试:

       it('should be able to render "BLAHBLAH " button ', () => 
         window.open = jest.fn();
         const BLAH = 'https://www.google.com/'
         const  getByText  = render(<Blah/>) // get text by my page Blah
         const buttonGoToBlah = getByText('BLAHBLAH') // get button by text
         fireEvent.click(buttonGoToBlah) // simulate the click event 
    
         expect(window.open).toHaveBeenCalledTimes(1) \\ expect the window.open have to been called at least once.
         expect(window.open).toHaveBeenCalledWith(BLAH, '_blank'); // and the page should be the same called in my BLAH page
      )

【讨论】:

【参考方案16】:

Jest 中的 window 对象是自嘲的

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

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

答案出人意料地不言自明:window 对象已经被模拟,可以毫不费力地引用它。这是由于jest 的现代版本默认使用jsdom 环境。

来自docs:

Jest 附带 jsdom,它模拟 DOM 环境,就像您在浏览器中一样。这意味着我们调用的每个 DOM API 都可以像在浏览器中一样被观察!

这里有一个非常简单的例子来说明这个概念:

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

诚然,这并没有涉及到 window.open,但由于其他人可能会在此页面上寻找有关如何模拟 window 本身的信息,我希望这里的答案会有所帮助。

【讨论】:

以上是关于如何使用 Jest 模拟 JavaScript 窗口对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 javascript 中模拟内部 JSON 对象?

javascript 模拟Jest功能事件

如何使用 jest.fn() 在 jest 中使用 typescript 模拟函数

如何使用 jest.fn() 模拟属性

如何使用 Jest 模拟异步函数

使用 jest.mock('axios') 时如何模拟拦截器?