【问题标题】:How can i access the history object inside of this promise call?我如何访问这个 Promise 调用中的历史对象?
【发布时间】:2021-05-13 17:47:30
【问题描述】:

在 React 组件中,按钮提交获取页面上文本字段的值并将它们传递给 mockFetch。然后,如果 mockFetch 的 promise 成功,它会触发 history.push('/newaccount')

我将测试设置为单击按钮,然后尝试使用我传递给它的内容来检测history.push('/newaccount') 调用,但我的模拟history.push 没有被调用。有谁知道我可以做些什么来让这个测试通过?

编辑:事实证明将当前的 jest.mock 替换为:

    const history = createMemoryHistory();
    const pushSpy = await jest.spyOn(history, "push");

允许我在 history.push 位于 mockFetch 函数之外时调用模拟历史记录......但当它位于函数内部时则不行。这就是我现在想要弄清楚的。

主应用路由:

function App() {
  const classes = useStyles();

  return (
    <div className="App">
      <Banner />
      <div id="mainSection" className={classes.root}>
        <ErrorBoundary>
          <Paper>
            <Router history={history}>
              <Switch>
                <Route path="/newaccount">
                  <NewAccountPage />
                </Route>
                <Route path="/disqualify">
                  <DisqualificationPage />
                </Route>
                <Route path="/">
                  <LandingPage />
                </Route>
              </Switch>
            </Router>
          </Paper>
        </ErrorBoundary>
      </div>
    </div>
  );
}

export default App;

组件:(省略了一些多余的字段)

import React, { useCallback, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import TextFieldStyled from "../TextFieldStyled/TextFieldStyled.js";
import * as actionTypes from "../../redux/actions/rootActions.js";
import {
  checkAllErrors,
} from "../../validators/validators.js";
import mockFetch from "../../fetchCall/fetchCall";
import "./LandingPage.css";

const LandingPage = () => {
  const dispatch = useDispatch();
  const history = useHistory();

  const {
    changeCarPrice,
    changeErrorMessage,
    resetState,
  } = actionTypes;

  useEffect(() => {
    return () => {
      dispatch({ type: resetState });
    };
  }, [dispatch, resetState]);

  const { carPrice } = useSelector(
    (state) => state
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!checkAllErrors(allErrors)) {
      // Call the API
      mockFetch(carPrice)
        .then((response) => {
          history.push("/newaccount");
        })
        .catch((error) => {
          dispatch({ type: changeErrorMessage, payload: error });
          history.push("/disqualify");
        });
    }
  };

  const [allErrors, setAllErrors] = useState({
    carValueError: false,
  });

  return (
    <div id="landingPage">
      <Grid container>
        <Grid item xs={2} />
        <Grid item xs={8}>
          <form onSubmit={handleSubmit}>
            <Grid container id="internalLandingPageForm" spacing={4}>
              {/* Text Fields */}
              <Grid item xs={6}>
                <TextFieldStyled
                  info={"Enter Car Price ($):"}
                  value={carPrice}
                  adornment={"$"}
                  type="number"
                  label="required"
                  required
                  error={allErrors.carValueError}
                  id="carPriceField"
                  helperText={
                    allErrors.carValueError &&
                    "Please enter a value below 1,000,000 dollars"
                  }
                  passbackFunction={(e) => handleChange(e, changeCarPrice)}
                />
              </Grid>
            </Grid>
            <Grid container id="internalLandingPageFormButton" spacing={4}>
              <Grid item xs={4} />
              <Grid item xs={3}>
                <Button
                  variant="contained"
                  color="primary"
                  type="submit"
                  id="applyNowButton"
                  title="applyNowButton"
                  onSubmit={handleSubmit}
                >
                  Apply Now
                </Button>
              </Grid>
              <Grid item xs={5} />
            </Grid>
          </form>
        </Grid>
        <Grid item xs={2} />
      </Grid>
    </div>
  );
};

export default LandingPage;

测试:

const wrapWithRedux = (component) => {
  return <Provider store={store}>{component}</Provider>;
};

  it("simulates a successful submission form process", () => {
    const mockHistoryPush = jest.fn();
    jest.mock("react-router-dom", () => ({
      ...jest.requireActual("react-router-dom"),
      useHistory: () => ({
        push: mockHistoryPush,
      }),
    }));

    render(
      wrapWithRedux(
        <Router history={history}>
          <Switch>
            <Route path="/newaccount">
              <NewAccountPage />
            </Route>
            <Route path="/">
              <LandingPage />
            </Route>
          </Switch>
        </Router>
      )
    );

    const carPriceField= screen.getByTestId("carPriceField");
    fireEvent.change(carPriceField, { target: { value: "5000" } });

    const buttonSubmission= screen.getByTitle("buttonSubmission");
    fireEvent.click(buttonSubmission);


    expect(mockHistoryPush).toHaveBeenCalledWith('/newaccount');
  });

【问题讨论】:

  • 您能否在if (!checkAllErrors(allErrors)) {...} 块内添加console.log 语句以确保它确实调用mockFetch
  • @ArunKumarMohan 刚刚将它添加到 mockFetch 的 .then 中,它确实被调用了。所以代码确实到达了 history.push("/newaccount")
  • 酷。我猜期望在mockFetch 函数返回的承诺解决之前运行。您可以通过注释掉mockFetch 调用并在if 条件中添加history.push 来测试这一点。如果你可以在你的代码中添加一个 CodeSandbox URL,它会更容易调试。
  • @ArunKumarMohan 嗯,如果我将 history.push 移出 mockFetch 调用,我仍然会遇到与从未调用过的 jest.fn() 相同的错误。我开始认为这就是 history.push 被嘲笑的方式。
  • 我在这方面取得了一些进展。如果我这样做:``` const history = createMemoryHistory(); const pushSpy = jest.spyOn(history, "push"); ``` 然后我可以在 mockFetch 之外访问模拟的 history.push。但如果它在 promise 函数 mockFetch 中......我不能。我认为这与它的承诺有关。

标签: javascript reactjs react-testing-library


【解决方案1】:

所以在进一步研究了嘲笑历史之后,我从How to mock useHistory hook in jest?找到了这个

答案似乎是做下面的代码然后会调用pushSpy

    const history = createMemoryHistory();
    const pushSpy = jest.spyOn(history, "push");

    render(
      wrapWithRedux(
        <Router history={history}>
          <Switch>
            <Route path="/newaccount">
              <NewAccountPage />
            </Route>
            <Route path="/">
              <LandingPage />
            </Route>
          </Switch>
        </Router>
      )
    );

我还必须使用 react 测试库中的 waitFor 包装期望行,以获取 history.push 以调用 mockFetch 函数内部的模拟,如下所示:

await waitFor(() =&gt; expect(pushSpy).toHaveBeenCalledWith("/newaccount"));

通过这两个修改,现在测试通过了。

【讨论】:

    【解决方案2】:

    我不会模拟一堆东西并测试实现,而是测试行为。

    这里有一些想法可能有助于编写行为而不是实现细节。

    参考 React Router 测试文档

    React Router 有一个基于操作的guide on testing navigation

    劫持 render() 以使用 Redux 进行包装

    您可以劫持react-testing-libraryrender() 函数来获取干净状态或种子状态,而不是为每个测试套件编写Redux 包装器。 Redux has docs here.

    不要嘲笑fetch()

    按钮提交获取页面上文本字段的值并将它们传递给 mockFetch

    我会使用 HTTP 拦截器来存根响应。通过这种方式,您可以获得异步行为,并将测试绑定到后端,而不是将其绑定到工具。假设你不喜欢fetch(),你会一直坚持下去,直到你迁移所有东西。我在Testing components that make API calls这个主题上发表了一篇博文。

    这是您的代码示例,并进行了一些修改:

      it("creates a new account", () => { // <-- more descriptive behavior
        // Stub the server response
        nock(`${yoursite}`)
          .post('/account/new') // <-- or whatever your backend is
          .reply(200);
    
        render(
            <MemoryRouter initialEntries={["/"]}> // <-- Start where your forms are at
              <Switch>
                <Route path="/newaccount">
                  <NewAccountPage />
                </Route>
                <Route path="/">
                  <LandingPage />
                </Route>
              </Switch>
            </MemoryRouter>
        );
    
        const carPriceField= screen.getByTestId("carPriceField");
        fireEvent.change(carPriceField, { target: { value: "5000" } });
    
        const buttonSubmission= screen.getByTitle("buttonSubmission");
        fireEvent.click(buttonSubmission);
    
    
        expect(document.body.textContent).toBe('New Account Page'); // <-- Tests route change
      });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-23
      • 2014-06-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多