【问题标题】:How can I mock the JavaScript window object using Jest?如何使用 Jest 模拟 JavaScript 窗口对象?
【发布时间】:2019-10-13 00:55:25
【问题描述】:

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

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]

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

【问题讨论】:

标签: javascript mocking jestjs


【解决方案1】:

用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 stackoverflow.com/questions/59173156/…
  • 谢谢!几个小时后,我只需要将window 更改为global
【解决方案2】:

你可以试试这个:

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

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

      【解决方案4】:

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

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

      参考:setupFilesAfterEnv [array]

      【讨论】:

        【解决方案5】:

        以下方法对我有用。这种方法允许我测试一些在浏览器和 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。
        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

          在 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}`);
            }
          }
          

          【讨论】:

            【解决方案8】:

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

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

            【讨论】:

              【解决方案9】:

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

              【讨论】:

                【解决方案10】:

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

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

                【讨论】:

                • 谢谢这是最好的方法!
                • 干净简洁!太棒了!
                【解决方案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】:

                  简单试试

                  let windowOpenSpy: jest.SpyInstance;
                  beforeEach(() => {
                      windowOpenSpy = jest.spyOn(window, 'open');
                  });
                  
                  it('should open window with dashboard url', () => {
                      expect(windowOpenSpy).toBeCalledWith('your url', '_blank');
                  });
                  

                  【讨论】:

                    【解决方案13】:
                    const windowSpy = jest.spyOn(iFrame, "contentWindow", "get");
                    windowSpy.mockImplementation(() => ({
                      location: {
                        origin: "https://test.com",
                        href: "href",
                        hash: "hash"
                      }
                    }));
                    

                    【讨论】:

                      【解决方案14】:

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

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

                      【讨论】:

                        【解决方案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 本身的信息,我希望这里的答案会有所帮助。

                          【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 2017-12-20
                            • 1970-01-01
                            • 2018-10-01
                            • 1970-01-01
                            相关资源
                            最近更新 更多