How to test decorated React component with shallow rendering

后端 未结 5 967
旧巷少年郎
旧巷少年郎 2020-12-15 19:45

I am following this tutorial: http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/

Trying to learn how \"shallow rendering\" works.

相关标签:
5条回答
  • 2020-12-15 20:18

    I think the above example is confusing because the decorator concept is used interchangeably with idea of a "higher order component". I generally use them in combination which will make testing/rewire/mocking easier.

    I would use decorator to:

    • Provide props to a child component, generally to bind/listen to a flux store

    Where as I would use a higher order component

    • to bind context in a more declarative way

    The problem with rewiring is I don't think you can rewire anything that is applied outside of the exported function/class, which is the case for a decorator.

    If you wanted to use a combo of decorators and higher order components you could do something like the following:

    //withMui-decorator.jsx
    function withMUI(ComposedComponent) {
      return class withMUI extends Component {
        constructor(props) {
          super(props);
          this.state = {
            store1: ///bind here based on some getter
          };
        }
        render() {
          return <ComposedComponent {...this.props} {...this.state} {...this.context} />;
        }
      };
    }
    
    //higher-order.jsx
    export default function(ChildComp) {
    
      @withMui  //provide store bindings
      return class HOC extends Component {
        static childContextTypes = {
          getAvatar: PropTypes.func
        };
    
        getChildContext() {
          let {store1} = this.props;
    
          return {
            getAvatar: (id) => ({ avatar: store1[id] });
          };
        }
      }
    }
    
    //child.js
    export default Child extends Component {
      static contextTypes = {
        getAvatar: PropTypes.func.isRequired
      };
      handleClick(id, e) {
        let {getAvatar} = this.context;
    
        getAvatar(`user_${id}`);
      }
      render() {
        let buttons = [1,2,3].map((id) => {
          return <button type="text" onClick={this.handleClick.bind(this, id)}>Click Me</button>
        });
    
        return <div>{buttons}</div>;  
      }
    }
    
    //index.jsx
    import HOC from './higher-order';
    import Child from './child';
    
    let MyComponent = HOC(Child);
    React.render(<MyComponent {...anyProps} />, document.body);
    

    Then when you want to test you can easily "rewire" your stores supplied from the decorator because the decorator is inside of the exported higher order component;

    //spec.js
    import HOC from 'higher-order-component';
    import Child from 'child';
    
    describe('rewire the state', () => {
      let mockedMuiDecorator = function withMUI(ComposedComponent) {
        return class withMUI extends Component {
          constructor(props) {
            super(props);
            this.state = {
              store1: ///mock that state here to be passed as props
            };
          }
          render()  {
            //....
          }
        }
      }
    
      HOC.__Rewire__('withMui', mockedMuiDecorator);
      let MyComponent = HOC(Child);
    
      let child = TestUtils.renderIntoDocument(
        <MyComponent {...mockedProps} />
      );
    
      let childElem = React.findDOMNode(child);
      let buttons = childElem.querySelectorAll('button');
    
      it('Should render 3 buttons', () => {
        expect(buttons.length).to.equal(3);
       });
    
    });
    

    I'm pretty sure this doesn't really answer your original question but I think you are having problems reconciling when to use decorators vs.higher order components.

    some good resources are here:

    • http://jaysoo.ca/2015/06/09/react-contexts-and-dependency-injection/
    • https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
    • https://github.com/badsyntax/react-seed/blob/master/app/components/Menu/tests/Menu-test.jsx
    • https://github.com/Yomguithereal/baobab-react/blob/master/test/suites/higher-order.jsx
    0 讨论(0)
  • 2020-12-15 20:23

    You can use 'babel-plugin-remove-decorators' plugin. This solution will let you write your components normally without exporting decorated and un-decorated components.

    Install the plugin first, then create a file with the following content, let us call it 'babelTestingHook.js'

    require('babel/register')({
     'stage': 2,
     'optional': [
      'es7.classProperties',
      'es7.decorators',
      // or Whatever configs you have
      .....
    ],
    'plugins': ['babel-plugin-remove-decorators:before']
    });
    

    and running your tests like below will ignore the decorators and you will be able to test the components normally

    mocha ./tests/**/*.spec.js --require ./babelTestingHook.js --recursive
    
    0 讨论(0)
  • 2020-12-15 20:28

    You can't. First let's slightly desugar the decorator:

    let PlayerProfile = withMUI(
        class PlayerProfile extends React.Component {
          // ...
        }
    );
    

    withMUI returns a different class, so the PlayerProfile class only exists in withMUI's closure.

    This is here's a simplified version:

    var withMUI = function(arg){ return null };
    var PlayerProfile = withMUI({functionIWantToTest: ...});
    

    You pass the value to the function, it doesn't give it back, you don't have the value.

    The solution? Hold a reference to it.

    // no decorator here
    class PlayerProfile extends React.Component {
      // ...
    }
    

    Then we can export both the wrapped and unwrapped versions of the component:

    // this must be after the class is declared, unfortunately
    export default withMUI(PlayerProfile);
    export let undecorated = PlayerProfile;
    

    The normal code using this component doesn't change, but your tests will use this:

    import {undecorated as PlayerProfile} from '../src/PlayerProfile';
    

    The alternative is to mock the withMUI function to be (x) => x (the identity function). This may cause weird side effects and needs to be done from the testing side, so your tests and source could fall out of sync as decorators are added.

    Not using decorators seems like the safe option here.

    0 讨论(0)
  • 2020-12-15 20:36

    Use Enzyme to test higher order / decorators with Shallow with a method called dive()

    Follow this link, to see how dive works

    https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/dive.md

    So you can shallow the component with higher order and then dive inside.

    In the above example :

    const wrapper=shallow(<PlayerProfile name={name} avatar={}/>)
    expect(wrapper.find("PlayerProfile").dive().find(".player-profile").length).toBe(1)
    

    Similarly you can access the properties and test it.

    0 讨论(0)
  • 2020-12-15 20:39

    In my case decorators are very useful and I dont want to get rid of them (or return wrapped and unwrapped versions) im my application.

    The best way to do this in my opinion is to use the babel-plugin-remove-decorators (which can be used to remove them in tests) has Qusai says, but I wrote the pre-processor differently like below:

    'use strict';
    
    var babel = require('babel-core');
    
    module.exports = {
      process: function(src, filename) {
        // Ignore files other than .js, .es, .jsx or .es6
        if (!babel.canCompile(filename)) {
         return '';
        }
    
        if (filename.indexOf('node_modules') === -1) {
          return babel.transform(src, {
            filename: filename, 
            plugins: ['babel-plugin-remove-decorators:before']
          }).code;
        }
    
        return src;
      }
    };
    

    Take notice of the babel.transform call that im passing the babel-plugin-remove-decorators:before element as an array value, see: https://babeljs.io/docs/usage/options/

    To hook this up with Jest (which is what I used), you can do it with settings like below in your package.json:

    "jest": {
      "rootDir": "./src",
      "scriptPreprocessor": "../preprocessor.js",
      "unmockedModulePathPatterns": [
        "fbjs",
        "react"
      ]
    },
    

    Where preprocessor.js is the name of the preprocessor.

    0 讨论(0)
提交回复
热议问题