【问题标题】:Jest: Testing window.location.reload笑话:测试 window.location.reload
【发布时间】:2019-09-06 19:41:12
【问题描述】:

我如何编写一个测试来确保reloadFn 方法确实重新加载了窗口?我找到了this resource,但我不清楚在给定函数中发生窗口重新加载时如何在编写测试时期望窗口重新加载。感谢您的帮助!

const reloadFn = () => {
  window.location.reload(true);
}

【问题讨论】:

  • 大多数断言库都包含帮助函数,帮助确定一个函数被调用了多少次,以便您的测试可以调用它,然后检查它是否被调用了适当的次数。见:chaijs.com/plugins/chai-spiesexpect(spy).to.have.been.called.exactly(3);
  • 我觉得通过单元测试你只需要确保该函数被调用并期望 window.location.reload 正常运行。你通常不会测试一个窗口是否真的被重新加载了,只是调用了重新加载
  • 对于 TypeScript,请参阅answer

标签: javascript jestjs


【解决方案1】:

更新答案(2021 年 11 月)

包装: "jest": "^26.6.0" "@testing-library/jest-dom": "^5.11.4"

构建:create-react-app 4

describe("test window location's reload function", () => {
  const original = window.location;

  const reloadFn = () => {
    window.location.reload(true);
  };

  beforeAll(() => {
    Object.defineProperty(window, 'location', {
      configurable: true,
      value: { reload: jest.fn() },
    });
  });

  afterAll(() => {
    Object.defineProperty(window, 'location', { configurable: true, value: original });
  });

  it('mocks reload function', () => {
    expect(jest.isMockFunction(window.location.reload)).toBe(true);
  });

  it('calls reload function', () => {
    reloadFn(); // as defined above..
    expect(window.location.reload).toHaveBeenCalled();
  });
});

注意:更新了答案,因为 CRA 中使用的最新 jest 版本不支持 old answer


旧答案

这是解决方案,但为了更好的组织而进行了重构:

describe('test window location\'s reload function', () => {
  const { reload } = window.location;

  beforeAll(() => {
    Object.defineProperty(window.location, 'reload', {
      configurable: true,
    });
    window.location.reload = jest.fn();
  });

  afterAll(() => {
    window.location.reload = reload;
  });

  it('mocks reload function', () => {
    expect(jest.isMockFunction(window.location.reload)).toBe(true);
  });

  it('calls reload function', () => {
    reloadFn(); // as defined above..
    expect(window.location.reload).toHaveBeenCalled();
  });
});

谢谢:)

【讨论】:

  • 其中一个版本肯定发生了重大变化,因为现在会抛出TypeError: Cannot assign to read only property 'reload' of object '[object Location]'。我添加了一个我发现可行的解决方案的答案(这个线程中也有其他好的解决方案)。
  • 在我更新的答案中修复了上述错误..
【解决方案2】:

如果您将 TypeScript 与 Jest 一起使用:

想法

  1. 创建一个副本,然后删除windowlocation 属性。
  2. 现在使用模拟的 reload 函数设置 location 属性。
  3. 测试完成后将原始值设置回。

代码:TypeScript 3.x 及以下

const location: Location = window.location;
delete window.location;
window.location = {
    ...location,
    reload: jest.fn()
};

// <code to test>
// <code to test>
// <code to test>

expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;

代码:TypeScript 4+

TypeScript 4 有更严格的检查(这是一件好事),所以我不确定除了使用 @ts-ignore@ts-expect-error 来抑制错误之外是否还有其他方法。

警告:禁止 TypeScript 验证可能是?危险的?。

const location: Location = window.location;

// WARNING:
//     @ts-ignore and @ts-expect-error suppress TypeScript validations by ignoring errors.
//     Suppressing TypeScript validations can be ?dangerous?.

// @ts-ignore
delete window.location;

window.location = {
    ...location,
    reload: jest.fn()
};

// <code to test>
// <code to test>
// <code to test>

expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;

【讨论】:

  • 这是我发现的唯一可以与 typescript 一起使用的解决方案。伟大的工作。
  • Dude 这应该是公认的答案,因为 window.location.reload 是只读的且不可配置。好东西!!
  • delete window.location 在 typescript 中导致错误“'delete' 运算符的操作数必须是可选的”。
  • 使用 jest 26.4.2 和 typescript 3.7.2 这是唯一对我有用的答案。当前接受的答案未能写入分配给window.location.reload
  • @Ken 我已经更新了答案以添加适用于 TypeScript 4 的代码,但恐怕我没有好的解决方案
【解决方案3】:

您还可以将 Murtaza Hussain 的解决方案简化为

  describe('refreshPage', () => {
    const { reload } = window.location;

    beforeAll(() => {
      Object.defineProperty(window, 'location', {
        writable: true,
        value: { reload: jest.fn() },
      });
    });

    afterAll(() => {
      window.location.reload = reload;
    });

    it('reloads the window', () => {
      refreshPage();
      expect(window.location.reload).toHaveBeenCalled();
    });
  });

【讨论】:

    【解决方案4】:

    您可以使用 sessionStorage 为每次重新加载保存一个值。 只要浏览器不关闭,该值就会保留在 sessionStorage 中。 当页面重新加载时,该值将增加。使用此值验证新重新加载。 通过将 reloadFn() 粘贴到控制台来测试它。 控制台将显示重新加载计数:1,并随着每次重新加载而增加。

    const reloadFn = () => {
      window.location.reload(true);
    }
    
    window.onload = function() {
        // get reloadCount from sessionStorage
        reloadCount = sessionStorage.getItem('reloadCount');
    
        // reloadCount will be null the first page load or a new value for each reload
        if (reloadCount) {
            // increment reloadCount
            reloadCount = parseInt(reloadCount) + 1;
            // save the new value to sessionStorage
            sessionStorage.setItem('reloadCount', reloadCount);
            console.log("Reload count: " + reloadCount);
        } else {
            // if reloadCount was null then set it to 1 and save to sessionStorage
            sessionStorage.setItem('reloadCount', 1);
            console.log("Page was loaded for the first time");
        }
    }
    

    【讨论】:

      【解决方案5】:

      在您的函数测试reloadFn 中,您应该使用您链接到的模拟代码:

      Object.defineProperty(window.location, 'reload', {
          configurable: true,
      }); // makes window.location.reload writable
      window.location.reload = jest.fn(); // set up the mock
      reloadFn(); // this should call your mock defined above
      expect(window.location.reload).toHaveBeenCalled(); // assert the call
      window.location.reload.mockRestore(); // restore window.location.reload to its original function
      

      为了更完善的测试,你可以使用

      expect(window.location.reload).toHaveBeenCalledWith(true);
      

      值得注意的是,这实际上并没有验证窗口是否已重新加载,这超出了单元测试的范围。诸如浏览器测试或集成测试之类的东西可以验证这一点。

      【讨论】:

      【解决方案6】:

      您可以通过以下方式使用原生 Jest 的 spyOn 函数,而不是使用 Object.defineProperty 的变通方法:

      test("reload test", () => {
        const { getByText } = renderComponentWithReloadButton()
      
        const reload = jest.fn()
      
        jest
          .spyOn(window, "location", "get")
          .mockImplementation(() => ({ reload } as unknown as Location))
      
        // Call an action that should trigger window.location.reload() function
        act(() => {
          getByText("Reload me").click()
        })
      
        // Test if `reload` function was really called
        expect(reload).toBeCalled()
      })
      

      还要确保在测试后使用 jest.clearAllMocks() 函数清除模拟。

      【讨论】:

        【解决方案7】:

        如果有人在 2020 年查找此内容,那么我也有同样的问题。

        为什么某些人会出现问题而其他人不会?

        这一切都取决于您运行的 chrome 版本,我为最终调用 window.location.reload 的组件编写了一个测试。下面是部分组件代码:

        onConfirmChange() {
            const {data, id} = this.state;
        
            this.setState({showConfirmationModal: false}, () => {
                this.update(data, id)
                  .then(() => window.location.reload());
            });
        }
        

        测试最初在我使用 chrome 版本 71 的构建服务器上失败,它在我的本地使用 chrome 版本 79 时通过了。 我今天将我的 chrome 更新到 84 版,问题出现在我的本地 似乎不支持删除 window.local。尝试了我可以在谷歌找到的所有解决方案,但没有任何效果。

        那么解决办法是什么?

        实际上非常简单,对于反应测试,我的系统使用酶,所以我所做的是将 window.location.reload 包装在实例方法中并在测试中存根

        JSX 代码:

        reloadWindow() {
            window.location.reload();
        }
        
        onConfirmChange() {
            const {data, id} = this.state;
          
            this.setState({showConfirmationModal: false}, () => {
              this.update(data, id)
                .then(() => reloadWindow());
            });   
        }
        

        测试

        it('check what happened', () => {
            render();
            const component = wrapper.instance();
            sandbox.stub(component, 'reloadWindow').callsFake();
        });
        

        【讨论】:

          【解决方案8】:

          更新答案(2021 年 5 月)

          我在线程中的很多答案中遇到了一些问题。我认为随着时间的推移对底层库的版本更改导致了损坏。

          我的配置:

          • "typescript": "~4.1.5"
          • "jest": "^26.6.3"
          • "jest-environment-jsdom": "^26.6.2"

          另外,我应该注意,我的解决方案非常冗长。但是我的用例需要测试window.location.replace() 和结果。所以我不能 简单地模拟window.location.replace。如果您只需要模拟其中一个函数而不关心实际的href 的变化,那么线程中的一些解决方案将使用更少的代码工作得很好。

          工作版本

          我发现完全填充 window.location 对象解决了我所有的问题。

          window.locationpolyfill

          使用此代码并将其放在测试文件或设置文件中的任何位置:

          export class MockWindowLocation {
            private _url: URL = new URL();
          
            get href (): string {
              return this._url.toString();
            }
          
            set href (url: string) {
              this._url = new URL(url);
            }
          
            get protocol (): string {
              return this._url.protocol;
            }
          
            get host (): string {
              return this._url.host;
            }
          
            get hostname (): string {
              return this._url.hostname;
            }
          
            get origin (): string {
              return this._url.origin;
            }
          
            get port (): string {
              return this._url.port;
            }
          
            get pathname (): string {
              return this._url.pathname;
            }
          
            get hash (): string {
              return this._url.hash;
            }
          
            get search (): string {
              return this._url.search;
            }
          
            replace = jest.fn().mockImplementation((url: string) => {
              this.href = url;
            });
          
            assign = jest.fn().mockImplementation((url: string) => {
              this.href = url;
            });
          
            reload = jest.fn();
          
            toString(): string {
              return this._url.toString();
            }
          }
          

          测试一下

          然后你必须删除window.location并将其设置为新的polyfill:

            it('should be able to test window.location', () => {
              delete window.location;
              Object.defineProperty(window, 'location', {
                value: new MockWindowLocation()
              });
          
              window.location.href = 'https://example.com/app/#/route/1';
              window.location.reload();
          
              expect(window.location.reload).toHaveBeenCalled();
              expect(window.location.href).toBe('https://example.com/app/#/route/1');
              expect(window.location.pathname).toBe('/app/');
              expect(window.location.hash).toBe('#/route/1');
            });
          

          这对我来说很神奇。希望它可以帮助其他人。

          其他答案更简单

          再次重申,此线程中还有其他可以正常工作的答案。我发现:

          Object.defineProperty(window, 'location', {
            writable: true,
            value: { reload: jest.fn() },
          });
          

          还有:

          const location: Location = window.location;
          delete window.location;
          window.location = {
            ...location,
            reload: jest.fn()
          };
          

          两者都有帮助。但就像我说的,我需要监视 replace() 并且仍然拥有 window.location 的标准功能。

          希望这对某人有所帮助。干杯!

          【讨论】:

            猜你喜欢
            • 2018-08-09
            • 2019-08-17
            • 2021-06-13
            • 1970-01-01
            • 2019-07-05
            • 2018-12-23
            • 2020-12-15
            • 2019-04-05
            • 2020-07-12
            相关资源
            最近更新 更多