简体   繁体   English

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

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

I am trying to test when the window.location.href changes after a button is clicked using react-testing-library.我正在尝试使用 react-testing-library 测试单击按钮后 window.location.href 何时更改。 I have seen examples online where you manually update window.location.href inside of a test case as so window.location.href = 'www.randomurl.com' and then follow it with expect(window.location.href).toEqual(www.randomurl.com) .我在网上看到了一些例子,你在测试用例中手动更新 window.location.href 就像window.location.href = 'www.randomurl.com'然后用expect(window.location.href).toEqual(www.randomurl.com) While this indeed will pass, I want to avoid this as I'd rather simulate the user actions instead of injecting the new value into the test.虽然这确实会通过,但我想避免这种情况,因为我宁愿模拟用户操作而不是将新值注入测试。 If I do that, even if I remove my button click (which is what will actually trigger the function call) the expect will still pass because I have anyway manually updated the window.location.href in my test如果我这样做,即使我删除了我的按钮点击(这实际上会触发函数调用)期望仍然会通过,因为我已经在我的测试中手动更新了 window.location.href

What I've opted for is having goToThisPage func (which will redirect the user) to be placed outside of my functional component.我选择的是将 goToThisPage 函数(它将重定向用户)放置在我的功能组件之外。 I then mock goToThisPage in my test file and in my test case check whether it has been called.然后我在我的测试文件中模拟 goToThisPage,并在我的测试用例中检查它是否被调用。 I do know that the goToThisPage is being triggered because I included a console.log and when I run my tests I see it in my terminal.我确实知道 goToThisPage 正在被触发,因为我包含了一个 console.log,当我运行测试时,我会在我的终端中看到它。 Nonetheless, the test still fails.尽管如此,测试仍然失败。 I have been playing around with both spyOn and jest.doMock/mock with no luck我一直在玩 spyOn 和 jest.doMock/mock 没有运气

component.js组件.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;

Test file:测试文件:

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

Note: when I try to just do jest.mock I got this error 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.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. 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. When testing out with jest.doMock the error disappeared but the actual test fails.使用jest.doMock进行测试时,错误消失了,但实际测试失败了。

I am open to hear more refined ideas of solving my issue if someone believes this solution could be improved.如果有人认为可以改进此解决方案,我愿意听取更完善的想法来解决我的问题。 Thanks in advance提前致谢

Edit: This is another approach I have tried out编辑:这是我尝试过的另一种方法

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

Save yourself the headache and split the goToThisPage function into its own file.省去您的麻烦,将 goToThisPage 函数拆分到它自己的文件中。 You seem to be mocking the goToThisPage function fine but when the Component is rendered with react testing library it doesn't seem render with the mocked function but defaults to what the function would normally do.您似乎很好地模拟了 goToThisPage 函数,但是当使用反应测试库呈现组件时,它似乎没有使用模拟函数呈现,而是默认为该函数通常执行的操作。 This easiest way would be just to mock the function from its own file.这个最简单的方法就是从它自己的文件中模拟函数。 If you truly want to keep the function in the same file you will need to make some adjustments, see (example #2) but I do not recommend this path.如果您真的想将函数保留在同一个文件中,则需要进行一些调整,请参见(示例 #2),但我不推荐此路径。

See below for examples请参阅下面的示例

Example 1: (Recommended) Split function into it's own file示例 1:(推荐)将函数拆分到它自己的文件中

Component.spec.jsx组件.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 goToThisPage.js

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

export default goToThisPage;

Component.jsx组件.jsx

import React from "react";
import ChildComponent from "./ChildComponent";
import goToThisPage from "./goToThisPage";

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

export default Component;

Example 2: (Not Recommend for React components!)示例 2:(不推荐用于 React 组件!)

We can also get it working by calling the goToThisPage function via exports.我们还可以通过导出调用 goToThisPage 函数来使其工作。 This ensures the component is rendered with our spyOn and mockImplementation.这确保组件使用我们的 spyOn 和 mockImplementation 呈现。 To get this working for both browser and jest you need to ensure we run the original function if it's on browser.要使此功能同时适用于浏览器和 jest,您需要确保我们在浏览器上运行原始功能。 We can do this by creating a proxy function that determines which function to return based on a ENV that jest defines when it runs.我们可以通过创建一个代理函数来做到这一点,该代理函数根据运行时定义的 ENV 确定返回哪个函数。

Component.jsx组件.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组件.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();
    });
});

You can move the function proxy to it's own file but you need to pass exports into the proxy function as exports is scoped to it's own file.您可以将函数代理移动到它自己的文件中,但您需要将导出传递给代理函数,因为导出的范围仅限于它自己的文件。

Example code示例代码

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

There are other ways to do this but I would follow the first solution as you should be splitting utility functions into it's own files anyway.还有其他方法可以做到这一点,但我会遵循第一个解决方案,因为无论如何您都应该将实用程序函数拆分到它自己的文件中。 Goodluck.祝你好运。

Example 3 for @VinceN @VinceN 的示例 3

You can mock a function that lives in the same file using the below example files.您可以使用以下示例文件模拟位于同一文件中的函数。

SomeComponent.tsx某些组件.tsx

import * as React from 'react';

const someFunction = () => 'hello world';

const SomeComponent = () => {
  return (
    <div data-testid="innards">
      {someFunction()}
    </div>
  )
}

export default SomeComponent;

SomeComponent.spec.tsx SomeComponent.spec.tsx

import SomeComponent from './SomeComponent';
import { render, screen } from "@testing-library/react";

jest.mock('./SomeComponent', () => ({
  __esModule: true,
  ...jest.requireActual('./SomeComponent'),
  someFunction: jest.fn().mockReturnValue('mocked!')
}));

describe('<SomeComponent />', () => {
  it('renders', () => {
      render(<SomeComponent />);
      const el = screen.getByTestId('innards');

      expect(el.textContent).toEqual('mocked!');
  });
});

You exporting both functions and then defining a default export of the Component itself is what's causing the problem (which is mixing up default and named exports).您导出这两个函数,然后定义Component本身的默认导出是导致问题的原因(混淆了默认导出和命名导出)。

Remove export default Component;删除export default Component; and change the top import in your test file to import {Component, goToThisPage} from './component' .并将测试文件中的顶部导入更改为import {Component, goToThisPage} from './component' That said I'm not sure you even need to export goToThisPage (for the Jest test at least).也就是说,我不确定您是否需要导出goToThisPage (至少对于 Jest 测试)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM