FE:单元测试 window.location.href 在按钮单击时发生变化(测试失败)

FE: Unit testing window.location.href changing on button click (test fails)

我正在尝试使用 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();
    });
});

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

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

省去您的麻烦,将 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;

Component.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,您需要确保我们 运行 原始功能(如果它在浏览器上)。我们可以通过创建一个代理函数来实现这一点,该代理函数根据一个 ENV 来确定 return 哪个函数,该 ENV 在 运行s.

时开玩笑地定义。

Component.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;

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