【问题标题】:FE: Unit testing window.location.href changing on button click (test fails)FE:单元测试 window.location.href 在按钮单击时更改(测试失败)
【发布时间】:2022-02-10 08:57:49
【问题描述】:

我正在尝试使用 react-testing-library 测试单击按钮后 window.location.href 何时更改。我在网上看到过一些示例,您可以手动更新测试用例中的 window.location.href,例如 window.location.href = 'www.randomurl.com',然后使用 expect(window.location.href).toEqual(www.randomurl.com) 进行更新。虽然这确实会通过,但我想避免这种情况,因为我宁愿模拟用户操作而不是将新值注入测试。如果我这样做,即使我删除了我的按钮单击(这实际上会触发函数调用),期望仍然会通过,因为无论如何我已经在我的测试中手动更新了 window.location.href

我选择将 goToThisPage 函数(它将重定向用户)放置在我的功能组件之外。然后我在我的测试文件中模拟 goToThisPage 并在我的测试用例中检查它是否已被调用。我确实知道 goToThisPage 正在被触发,因为我包含了一个 console.log 并且当我运行我的测试时,我在我的终端中看到了它。尽管如此,测试仍然失败。我一直在玩 spyOn 和 jest.doMock/mock 都没有运气

component.js

import React from 'react'
import { ChildComponent } from './childcomponent';
export const goToThisPage = () => {
  const url = '/url'
  window.location.href = url;
  console.log('reached');
};

export const Component = () => {
 return (<ChildComponent goToThisPage={ goToThisPage }/>)
}

export default Component;

测试文件:

  import * as Component from './component'
  import userEvent from '@testing-library/user-event';

  jest.doMock('./component', () => ({
   goToThisPage: jest.fn(),
  }));

  describe('goToThisPage', () => {
    test('should call goToThisPage when button is clicked', async () => {
        const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage');
        const { container, getByTestId } = render(<Component.Component />);

        userEvent.click(screen.getByTestId('goToThisPage')); // this is successfully triggered (test id exists in child component)
        expect(goToThisPageSpy).toHaveBeenCalled();
        // expect(Component.goToThisPage()).toHaveBeenCalled(); this will fail and say that the value must be a spy or mock so I opted for using spy above
    });
});

注意:当我尝试只做 jest.mock 时,我收到了这个错误Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports. 当使用jest.doMock 进行测试时,错误消失了,但实际测试失败了。

如果有人认为此解决方案可以改进,我愿意听取有关解决我的问题的更完善的想法。提前致谢

编辑: 这是我尝试过的另一种方法

  import { Component, goToThisPage } from './component'
  import userEvent from '@testing-library/user-event';

  describe('goToThisPage', () => {
    test('should call goToThisPage when button is clicked', async () => {
        const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage'); 
// I am not certain what I'd put as the first value in the spy. Because `goToThisPage` is an external func of <Component/> & not part of the component
        const { container, getByTestId } = render(<Component />);

        userEvent.click(screen.getByTestId('goToThisPage'));
        expect(goToThisPageSpy).toHaveBeenCalled();
    });
});

【问题讨论】:

    标签: reactjs unit-testing jestjs react-testing-library window.location


    【解决方案1】:

    免去您的麻烦,将 goToThisPage 函数拆分为自己的文件。您似乎很好地模拟了 goToThisPage 函数,但是当使用反应测试库呈现组件时,它似乎没有使用模拟函数呈现,而是默认为函数通常会执行的操作。这种最简单的方法就是从自己的文件中模拟函数。如果您确实想将函数保留在同一个文件中,则需要进行一些调整,请参见(示例 #2),但我不推荐此路径。

    请参阅下面的示例

    示例 1:(推荐)将函数拆分为自己的文件

    Component.spec.jsx

    import React from "react";
    import Component from "./Component";
    import { render, screen } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    import * as goToThisPage from "./goToThisPage";
    
    jest.mock('./goToThisPage');
    
    describe("goToThisPage", () => {
        test("should call goToThisPage when button is clicked", async () => {
            const goToThisPageSpy = jest.spyOn(goToThisPage, 'default').mockImplementation(() => console.log('hi'));
            render(<Component />);
            userEvent.click(screen.getByTestId("goToThisPage"));
            expect(goToThisPageSpy).toHaveBeenCalled();
        });
    });
    

    goToThisPage.js

    export const goToThisPage = () => {
        const url = "/url";
        window.location.href = url;
    };
    
    export default goToThisPage;
    

    组件.jsx

    import React from "react";
    import ChildComponent from "./ChildComponent";
    import goToThisPage from "./goToThisPage";
    
    export const Component = () => {
        return <ChildComponent goToThisPage={goToThisPage} />
    };
    
    export default Component;
    

    示例 2:(不推荐用于 React 组件!)

    我们还可以通过导出调用 goToThisPage 函数来使其工作。这确保了组件是使用我们的 spyOn 和 mockImplementation 呈现的。要让这个对浏览器和 jest 都有效,如果它在浏览器上,您需要确保我们运行原始函数。我们可以通过创建一个代理函数来做到这一点,该函数根据 jest 在运行时定义的 ENV 来确定返回哪个函数。

    组件.jsx

    import React from "react";
    import ChildComponent from "./ChildComponent";
    
    export const goToThisPage = () => {
        const url = "/url";
        window.location.href = url;
    };
    
    // jest worker id, if defined means that jest is running
    const isRunningJest = !!process.env.JEST_WORKER_ID;
    
    // proxies the function, if jest is running we return the function
    // via exports, else return original function. This is because
    // you cannot invoke exports functions in browser!
    const proxyFunctionCaller = (fn) => isRunningJest ? exports[fn.name] : fn;
    
    export const Component = () => {
        return <ChildComponent goToThisPage={proxyFunctionCaller(goToThisPage)} />
    };
    
    export default Component;
    

    Component.spec.jsx

    import React from "react";
    import { render, screen } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    
    describe("goToThisPage", () => {
        test("should call goToThisPage when button is clicked", async () => {
            const Component = require('./Component');
            const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage').mockImplementation(() => console.log('hi'));
    
            render(<Component.default />);
    
            userEvent.click(screen.getByTestId("goToThisPage"));
            expect(goToThisPageSpy).toHaveBeenCalled();
        });
    });
    

    您可以将函数代理移动到它自己的文件中,但您需要将导出传递给代理函数,因为导出的范围是它自己的文件。

    示例代码

    // component.js
    import React from "react";
    import ChildComponent from "./ChildComponent";
    import proxyFunctionCaller from "./utils/proxy-function-caller";
    
    export const goToThisPage = () => {
        const url = "/url";
        window.location.href = url;
    };
    
    export const Component = () => {
        return <ChildComponent goToThisPage={proxyFunctionCaller(typeof exports !== 'undefined' ? exports : undefined, goToThisPage)} />
    };
    
    export default Component;
    
    // utils/proxy-function-caller.js
    
    // jest worker id, if defined means that jest is running
    const isRunningJest = !!process.env.JEST_WORKER_ID;
    
    // proxies the function, if jest is running we return the function
    // via exports, else return original function. This is because
    // you cannot invoke exports functions in browser!
    const proxyFunctionCaller = (exports, fn) => isRunningJest ? exports[fn.name] : fn;
    
    export default proxyFunctionCaller;
    

    还有其他方法可以做到这一点,但我会遵循第一个解决方案,因为无论如何您都应该将实用程序函数拆分为它自己的文件。祝你好运。

    【讨论】:

      【解决方案2】:

      您导出这两个函数,然后定义 Component 本身的默认导出是导致问题的原因(混淆了默认导出和命名导出)。

      删除 export default Component; 并将测试文件中的顶部导入更改为 import {Component, goToThisPage} from './component'。也就是说,我不确定您是否需要导出 goToThisPage(至少对于 Jest 测试而言)。

      【讨论】:

      • 感谢您的评论。所以我这样做了,现在我的期望是expect(goToMyAccount).toHaveBeenCalled();。但我得到了错误:Matcher error: received value must be a mock or spy functionReceived has type: function Received has value: [Function goToMyAccount]。有什么建议我可能仍然做错了吗?如果需要,我可以在上面重新发布我的代码
      • @critical_maas - 我不知道您还更改了什么,但该错误意味着您没有在监视您要测试的功能。
      • 我在上面添加了我的代码现在的样子。我已经删除了 spyOn,因此我在之前的评论中提到了错误。我已经阅读了它,但这实际上是我第一次开始测试时的原始问题/不确定性。这也是我选择import * as Component from './component' 的原因 - 我会在 spyOn 中将什么作为第一个参数,因为 goToThisPage 是一个外部函数?再次感谢您的回答
      • @critical_maas - 我不确定goToThisPage(或goToMyAccount)是否需要导出到测试类。 spyOn 应该是正确的,因为Component 具有goToThisPage 的属性(设置为函数goToThisPage)。这也可能是expect 抱怨获取函数而不是模拟的原因 - 它正在获取您正在导入的函数。
      猜你喜欢
      • 2015-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 2020-01-14
      • 1970-01-01
      相关资源
      最近更新 更多