我如何访问这个 Promise 调用中的历史对象?

Posted

技术标签:

【中文标题】我如何访问这个 Promise 调用中的历史对象?【英文标题】:How can i access the history object inside of this promise call? 【发布时间】: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 中......我不能。我认为这与它的承诺有关。 【参考方案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
  );

【讨论】:

以上是关于我如何访问这个 Promise 调用中的历史对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何同步 Promise 对象?

如何修复 React 中的“类型错误:尝试访问对象的属性时无法读取未定义的属性名称”

javascript,promise,如何在 then 范围内访问变量 this [重复]

如何在构造函数中调用promise对象来设置属性[重复]

在 Vue 3 中将数据对象设置为来自 Promise 的值

在维护数据的同时链接 Promise(角度 js)