【问题标题】:Testing of Nested React components using React-Test-Renderer使用 React-Test-Renderer 测试嵌套的 React 组件
【发布时间】:2020-06-21 10:19:57
【问题描述】:

我有一个纯 React-Redux 应用程序,它按预期工作。

App.js

import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { Router, Route, Switch, Redirect } from "react-router-dom";

import history from "../history";
import LandingPage from "./home/LandingPage";
import { displayModules } from "../actions";
import Cart from "./home/Cart";

const App = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(displayModules());
  }, [dispatch]);

  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={LandingPage}></Route>
        <Route path="/cart" exact component={Cart}></Route>
        <Route render={() => <Redirect to="/" />} />
      </Switch>
    </Router>
  );
};

export default App;

LandingPage 有一个名为 Tile 的嵌套组件。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Tile from "../common/Tile";

import { addItemToCart, displayCartContents } from "../../actions";
import "./LandingPage.css";

const LandingPage = () => {
  const modules = useSelector(state => state.data.modules);
  const cart = useSelector(state => state.data.cart);
  const dispatch = useDispatch();
  const addToCart = item => {
    dispatch(addItemToCart(item));
  };
  return (
    <div className="app">
      <div className="header">
        <div className="text">Insurance modules</div>
        <i
          className="shopping cart icon"
          onClick={() => {
            dispatch(displayCartContents());
          }}
        >
          <span className="badge">{cart.length}</span>
        </i>
      </div>
      <div className="body">
        {modules.map(module => (
          <Tile key={module.id} module={module} addToCart={addToCart}></Tile>
        ))}
      </div>
    </div>
  );
};

export default LandingPage;

Tile.js 有一个我想测试的按钮。

import React, { useState } from "react";

import "./Tile.css";

const Tile = props => {
  const { module, addToCart } = props;
  const [coverage, setCoverage] = useState(parseInt(module.coverageMax - module.coverageMin) / 2);
  const [price, setPrice] = useState((coverage * module.risk) / 100);
  return (
    <div className="tile">
      <div className="tile-description">
        <div>
          <i className={`${module.icon} icon`}></i>
        </div>
        <div className="tile-name">{module.name}</div>
        <div className="tile-risk">Risk(%): {module.risk}</div>
      </div>
      <div className="tile-footer">
        <div className="tile-range">
          <div className="field-label">
            Select Coverage: <span className="coverage-display">{coverage}</span>
          </div>
          <div className="slidecontainer">
            <span className="slider-step">{module.coverageMin}</span>
            <input
              type="range"
              min={module.coverageMin}
              max={module.coverageMax}
              value={coverage}
              className="slider"
              onChange={e => {
                setCoverage(e.target.value);
                setPrice((e.target.value * module.risk) / 100);
              }}
            ></input>
            <span className="slider-step">{module.coverageMax}</span>
          </div>
        </div>
        <div>
          PRICE at this Coverage:<span className="tile-price">{price}</span>
        </div>

        <button
          className="tile-button"
          onClick={() => {
            addToCart({
              id: module.id,
              name: module.name,
              coverage: coverage,
              price: price,
              timeStamp: Math.ceil(new Date().getTime() * Math.random() * Math.random())
            });
          }}
        >
          Add module to cart
        </button>
      </div>
    </div>
  );
};

export default Tile;

App.test.js 工作正常,我可以通过 className 属性找到嵌套的着陆页 div。

import React from "react";
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
import renderer from "react-test-renderer";

import App from "../components/App";
import history from "../history";
import { displayModules } from "../actions";
import { DISPLAY_MODULES } from "../actions/types";

const mockStore = configureStore([]);

describe("App Component test", () => {
  let store = {};
  let wrappedComponent = {};
  const expectedActions = {
    type: DISPLAY_MODULES,
    payload: [
      {
        id: 0,
        icon: "bicycle",
        name: "Bike",
        coverageMin: 0,
        coverageMax: 3000,
        risk: 30
      },
      {
        id: 1,
        icon: "gem",
        name: "Jewelry",
        coverageMin: 500,
        coverageMax: 10000,
        risk: 5
      },
      {
        id: 2,
        icon: "microchip",
        name: "Electronics",
        coverageMin: 500,
        coverageMax: 6000,
        risk: 35
      },
      {
        id: 3,
        icon: "football ball",
        name: "Sports Equipment",
        coverageMin: 0,
        coverageMax: 20000,
        risk: 30
      }
    ]
  };
  beforeEach(() => {
    store = mockStore({
      data: {
        modules: [],
        cart: [],
        total: 0
      }
    });
    store.dispatch = jest.fn(displayModules);
    wrappedComponent = renderer.create(
      <Provider store={store}>
        <App />
      </Provider>
    );
  });

  it("should render with given state from Redux store", () => {
    expect(wrappedComponent.toJSON()).toMatchSnapshot();
  });
  it("should have an app from Landing Page", () => {
    expect(wrappedComponent.root.findByProps({ className: "app" })).toBeDefined();
  });

  it("should show landing page for default route", () => {
    *debugger;
    expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();*
  });
  it("should show cart page for /cart route", () => {
    history.push("/cart");
    expect(wrappedComponent.root.findByProps({ className: "backward icon" })).toBeDefined();
  });
  it("should redirect to landing page for unmatched 404 routes", () => {
    history.push("/someRandomRoute");
    expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();
  });
  it("should dispatch displayModules action on app mount", async () => {
    const actualAction = await store.dispatch();
    expect(actualAction).toEqual(expectedActions);
  });
});

但是如果你看到测试调试器

具有 className: body 的 div 的子级没有子级。 这就是它无法找到 Tile 组件的原因。 你能建议为什么孩子们对身体无效吗? 我以前见过这个,即使我尝试使用酶我也遇到了这个问题。 由于它是一个 Redux 包装的组件,我无法直接创建登陆页面或 Tile 组件进行测试。 如何测试嵌套项?

【问题讨论】:

  • I cant directly create the Landing page or Tile component for testing- 你可以通过注入你的依赖而不是在组件中声明它们来做到这一点
  • 我不确定如何注入依赖项,你能举个例子吗?
  • @arnaud 经过一番研究,我明白你的意思......但这意味着使用 Context 而不是 Redux 或使用其他方式使用 refractJs 或其他一些新库来处理组件连接......只是用于单元测试...嗯,这可能是重新设计整个应用程序以进行单元测试的解决方案...但是使用 Redux 是否有缺点?
  • 嗨,对不起,我没有太多时间。这周我会试着写一个合适的例子。这个想法是简单地将 redux 函数作为参数传递,而不是在组件中声明它们。这样,您就可以轻松地对其进行模拟并对其进行测试
  • @ArnaudClaudel,谢谢,如果你能告诉我,那就太好了。我也去看看。

标签: reactjs react-redux jestjs react-test-renderer


【解决方案1】:

您正在为 redux 状态中的模块提供一个空数组:

store = mockStore({
  data: {
    modules: [], // your modules is empty so no tiles will render
    cart: [],
    total: 0
  }
});

另一个问题是你模拟了 store.dispatch,因此即使调度了一些操作,它也不再更改 redux 存储:

store.dispatch = jest.fn(displayModules);

如果你想测试一个动作是否被调度,你可以使用:

const actions = store.getActions()

这将为您提供所有已调度的操作。

如果您想根据商店数据测试应用的呈现方式,您可以:

  1. 在测试中设置商店:
const existingModules = [ ... ]; // list of modules
store = mockStore({
  data: {
    modules: existingModules, 
    cart: [],
    total: 0
  }
});
  1. 您可以在测试中模拟 useSelector:
const existingModules = [ ... ]; // list of modules
const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue(existingModules)

【讨论】:

  • 谢谢@Tudor,你是对的,我现在可以找到 Tile 组件,这是我的测试逻辑的问题。使用模拟 useSelector 我也可以找到现有的模块。感谢您的帮助
  • 很高兴我能帮上忙! :)
猜你喜欢
  • 2020-02-12
  • 2021-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-19
  • 2021-01-27
  • 2017-04-12
相关资源
最近更新 更多