【问题标题】:How do you debug a shallow rendered enzyme test?您如何调试浅渲染酶测试?
【发布时间】:2020-08-26 06:03:57
【问题描述】:

我正在尝试修复我的 react-redux 应用程序中的失败测试。当我再次深入我的组件时,我希望在其中看到 JSX。但是,我什么也没看到。

这是我的组件 -

      const Dashboard = (props) => {
          if (props.isSignedIn)
            return (
              <div className="dashboard">
                  <h1>Welcome</h1>
              </div>
            );
          return null;
        };

    const mapStateToProps = (state) => {
      return { isSignedIn: state.auth.isSignedIn };
    };
export default connect(mapStateToProps, { signIn, signOut })(Dashboard);

这是我失败的测试:-

const setup = (initialState = {}) => {
  const store = storeFactory(initialState);
  const wrapper = shallow(<Dashboard store={store} />).dive().dive();
  return wrapper;
};

describe("on render", () => {
describe("the user is signed in", () => {
  let wrapper;
  beforeEach(() => {
    const initialState = { isSignedIn: true };
    wrapper = setup(initialState);
  });
  it("renders the dashboard", () => {
    const component = wrapper.find("dashboard");
    expect(component.length).toBe(1);
  });
});

我的商店工厂:-

export const storeFactory = (initialState) => {
  const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
  console.log(initialState);
  return createStoreWithMiddleware(rootReducer, initialState);
};

我的测试错误:-

● the user is signed in › renders the dashboard

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 0

当我潜水一次时,它看起来像这样:-

 <Dashboard store={{...}} isSignedIn={{...}} signIn={[Function]} signOut={[Function]} />}

但是当我尝试查看仪表板组件内部的 JSX 时,我什么也看不到?

【问题讨论】:

    标签: reactjs testing redux react-redux enzyme


    【解决方案1】:

    我很确定你的设置不起作用,因为你试图 shallow 挂载一个 redux 连接的组件——这是一个高阶组件 (HOC),它包装了另一个无法正确 @ 的组件987654326@d 由于酶的浅薄限制。

    相反,您有两种选择:

    选项 1.) 推荐:导出Dashboard 组件并使用shallowmount 对其进行断言(我主要使用mount 而不是shallow,专门用于避免过度和重复的.dive() 电话)。

    先导出未连接的组件:

    export const Dashboard = (props) => {
      if (props.isSignedIn)
        return (
          <div className="dashboard">
            <h1>Welcome</h1>
          </div>
        );
    
      return null;
    }
    
    export default connect(...)(Dashboard)
    

    然后在您的测试中,导入 Dashboard 组件(不是默认连接的导出):

    import { mount } from "enzyme";
    import { Dashboard } from "./Dashboard"; // importing named export "Dashboard"
    
    const initialProps = {
      isSignedIn: false
    }
    
    const wrapper = mount(<Dashboard { ...initialProps } />); // alternatively, you can use shallow here
    
    // now manipulate the wrapper using wrapper.setProps(...);
    

    选项 2.) 不推荐:用真正的storemount 连接的 HOC 将组件包装在真正的 Provider 中:

    import { mount } from "enzyme";
    import { Provider } from "redux";
    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import rootReducer from "../path/to/reducers";
    import types from "../path/to/types";
    import Dashboard from "./Dashboard"; // importing connected default export
    
    const store = createStore(rootReducer, undefined, applyMiddleware(thunk));
    
    const initialProps = {
      isSignedIn: false
    };
    
    const wrapper = mount(
      <Provider store={store}>
        <Dashboard { ...initialProps } />
      </Provider>
    );
    
    // now you'll dispatch real actions by type and expect the redux store to change the Dashboard component
    

    有关更多测试信息,请查看此answer,其中包含类似的示例(跳至要点;忽略 MemoryRouter 部分;虽然示例有点旧,但测试模式是一样)。

    我推荐选项 1 而不是选项 2 的原因是它更易于管理,因为您直接操作正在测试的组件(而不是包装组件的 HOC)。选项 2 仅在您绝对需要测试 redux 到连接组件的整个工作流程时才有用。我发现选项 2 大多是矫枉过正,因为您可以单独对每个部分(动作、reducers 和未连接的组件)进行单元测试并实现相同的测试覆盖率。此外,正如我在 example 中提到的,我发现 redux-mock-store 对于单元测试 redux 操作非常有用。

    顺便说一句,您可以通过在测试中使用console.log(wrapper.debug()); 查看酶的作用!


    未连接测试示例:

    import React, { createElement } from "react";
    import { configure } from "enzyme";
    import Adapter from "enzyme-adapter-react-16";
    import { mount } from "enzyme";
    import { MemoryRouter } from "react-router-dom";
    import { Dashboard } from "./App.js";
    
    configure({ adapter: new Adapter() });
    
    const initialProps = {
      isSignedIn: false
    };
    
    describe("Unconnected Dashboard Component", () => {
      let wrapper;
      beforeEach(() => {
        /* 
          This code below may be a bit confusing, but it allows us to use
          "wrapper.setProps()" on the root by creating a function that first
           creates a new React element with the "initialProps" and then
           accepts additional incoming props as "props". 
        */
        wrapper = mount(
          createElement(
            props => (
              <MemoryRouter initialEntries={["/"]}> // use memory router for testing (as recommended by react-router-dom docs: https://reacttraining.com/react-router/web/guides/testing)
                <Dashboard {...props} />
              </MemoryRouter>
            ),
            initialProps
          )
        );
      });
    
      it("initially displays nothing when a user is not signed in", () => {
        expect(wrapper.find(".dashboard").exists()).toBeFalsy();
      });
    
      it("displays the dashboard when a user is signed in", () => {
        wrapper.setProps({ isSignedIn: true });
        expect(wrapper.find(".dashboard").exists()).toBeTruthy();
      });
    });
    

    工作示例(单击Tests 选项卡运行测试):


    可重用的 HOC 包装器:

    utils/HOCWrapper/index.js

    import React, { createElement } from "react";
    import { createStore, applyMiddleware } from "redux";
    import { Provider } from "react-redux";
    import { MemoryRouter } from "react-router-dom";
    import { mount } from "enzyme";
    import thunk from "redux-thunk";
    import rootReducer from './path/to/reducers';
    
    const middlewares = applyMiddleware([thunk]);
    export const store = createStore(rootReducer, null, middlewares);
    
    /**
     * Factory function to create a mounted MemoryRouter + Redux Wrapper for a component
     * @function HOCWrapper
     * @param {node} Component - Component to be mounted
     * @param {object} initialProps - Component props specific to this setup.
     * @param {object} state - Component initial state for setup.
     * @param {array} initialEntries - Initial route entries for MemoryRouter.
     * @param {object} options - Optional options for enzyme's "mount"
     * @function createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root)
     * @returns {MountedRouterWrapper}
     */
    export const HOCWrapper = (
        Component,
        initialProps = {},
        state = null,
        initialEntries = ["/"],
        options = {},
    ) => {
        const wrapper = mount(
            createElement(
                props => (
                    <Provider store={store}>
                        <MemoryRouter initialEntries={initialEntries}>
                            <Component {...props} />
                        </MemoryRouter>
                    </Provider>
                ),
                initialProps,
            ),
            options,
        );
        if (state) wrapper.find(Component).setState(state);
        return wrapper;
    };
    
    export default HOCWrapper;
    

    要使用它,请导入 HOCWrapper 函数:

    import Component from "./Example";
    import HOCWrapper from "./path/to/utils/HOCWrapper"; 
    // if you need access to store, then... 
    // import HOCWrapper, { store } from "./path/to/utils/HOCWrapper";
    
    const initialProps = { ... };
    const initialState = { ... }; // optional (default null)
    const initialEntries = [ ... ]; // optional (default "/")
    const options = { ... }; // optional (default empty object)
    
    // use the JSDoc provided above for argument breakdowns
    const wrapper = HOCWrapper(Component, initialProps, initialState, initialEntries, options); // not all args are required, just the "Component"
    

    【讨论】:

    • 谢谢@mattcarlotta,选项 1 中的设置帮助了 :) 我在尝试更新我的 isSigned in 道具时遇到了一个小问题。这是我的代码const initialProps = { isSignedIn: false, }; describe("Unconnected Dashboard Component", () =&gt; { let wrapper; beforeEach(() =&gt; { wrapper = mount( &lt;Router&gt; &lt;Dashboard {...initialProps} /&gt; &lt;/Router&gt; ); console.log(wrapper.debug()); }); it("displays the dashboard", () =&gt; { wrapper.setProps({ isSignedIn: true }); expect(wrapper.find(".dashboard")).toHaveLength(1); }); });
    • 错误 - 未连接的仪表板组件 › 显示仪表板 expect(received).toHaveLength(expected) 预期长度:1 接收长度:0 接收对象:{} 22 | it("显示仪表板", () => { 23 | wrapper.setProps({ isSignedIn: true }); > 24 | expect(wrapper.find(".dashboard")).toHaveLength(1); | ^ 25 | }); 26 | }); console.log
    • 如果您不在组件中使用RouterBrowserRouter(除非您使用的是LinkuseRouter),您将不需要它。否则,您的测试应该按预期工作。
    • 更新了我上面关于未连接仪表板测试的答案(查看底部)。
    • 谢谢@mattcarlotta——你的例子很有意义谢谢——不幸的是,他们仍然失败了。对于第一个测试它 - Received: {} 并且第二个应该是 Received: false 它按预期手动工作,所以我真的不确定。当我 console.debug() 两次都给我 false - console.log ps我在组件中添加了一个链接。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-14
    • 2020-03-12
    • 1970-01-01
    • 1970-01-01
    • 2015-11-23
    • 1970-01-01
    • 2019-03-23
    相关资源
    最近更新 更多