如何使用浅层渲染测试装饰过的 React 组件

How to test decorated React component with shallow rendering

我正在学习本教程:http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/

正在尝试了解 "shallow rendering" 的工作原理。

我有一个高阶组件:

import React from 'react';

function withMUI(ComposedComponent) {
  return class withMUI {
    render() {
      return <ComposedComponent {...this.props}/>;
    }
  };
}

和一个组件:

@withMUI
class PlayerProfile extends React.Component {
  render() {
    const { name, avatar } = this.props;
    return (
      <div className="player-profile">
        <div className='profile-name'>{name}</div>
        <div>
          <Avatar src={avatar}/>
        </div>
      </div>
    );
  }
}

和一个测试:

describe('PlayerProfile component - testing with shallow rendering', () => {
  beforeEach(function() {
   let {TestUtils} = React.addons;

    this.TestUtils = TestUtils;

    this.renderer = TestUtils.createRenderer();
    this.renderer.render(<PlayerProfile name='user'
                                            avatar='avatar'/>);
  });

  it('renders an Avatar', function() {
    let result = this.renderer.getRenderOutput();
    console.log(result);
    expect(result.type).to.equal(PlayerProfile);
  });
});

result 变量持有 this.renderer.getRenderOutput()

在教程中,result.type 的测试方式如下:

expect(result.type).toEqual('div');

在我的例子中,如果我记录 result 它是:

LOG: Object{type: function PlayerProfile() {..}, .. }

所以我改变了我的测试:

expect(result.type).toEqual(PlayerProfile)

现在它给我这个错误:

Assertion Error: expected [Function: PlayerProfile] to equal [Function: withMUI]

所以PlayerProfile的类型是高阶函数withMUI.

PlayerProfile修饰withMUI,使用浅层渲染,只有PlayerProfile组件被渲染,而不是children。所以浅渲染不适用于我假设的装饰组件。

我的问题是:

为什么在教程中 result.type 应该是 div,但在我的例子中不是。

如何使用浅层渲染测试装饰有高阶组件的 React 组件?

你不能。首先让我们稍微去除装饰器的糖分:

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

withMUI returns 一个不同的 class,所以 PlayerProfile class 只存在于 withMUI 的闭包中。

这是一个简化版本:

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

你将值传递给函数,它没有返回,你没有值。

解决办法?保留对它的引用。

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

然后我们可以导出组件的包装和未包装版本:

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

使用这个组件的正常代码不会改变,但您的测试将使用这个:

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

另一种方法是将 withMUI 函数模拟为 (x) => x(标识函数)。这可能会导致奇怪的副作用,需要从测试端完成,因此您的测试和源代码可能会在添加装饰器时不同步。

这里不使用装饰器似乎是安全的选择。

我认为上面的示例令人困惑,因为 decorator 概念与 "higher order component" 的概念可互换使用。我通常将它们组合使用,这将使 testing/rewire/mocking 更容易。

我会使用装饰器来:

  • 向子组件提供 props,通常是 bind/listen 到 flux store

在哪里我会使用更高阶的组件

  • 以更具声明性的方式绑定上下文

重新布线的问题是我认为您不能重新布线在导出 function/class 之外应用的任何内容,装饰器就是这种情况。

如果您想使用装饰器和高阶组件的组合,您可以执行以下操作:

//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);

然后当你想测试时,你可以很容易地"rewire"你的商店由装饰器提供,因为装饰器在导出的高阶组件内;

//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);
   });

});

我很确定这并没有真正回答您最初的问题,但我认为您在协调何时使用装饰器时遇到问题 vs.higher 订购组件。

这里有一些很好的资源:

您可以使用 'babel-plugin-remove-decorators' 插件。此解决方案将让您正常编写组件,而无需导出装饰和未装饰的组件。

先安装插件,然后创建一个包含以下内容的文件,我们称之为'babelTestingHook.js'

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

和运行你的下面的测试将忽略装饰器,你将能够正常测试组件

mocha ./tests/**/*.spec.js --require ./babelTestingHook.js --recursive

在我的例子中,装饰器非常有用,我不想在我的应用程序中摆脱它们(或 return 包装和未包装的版本)。

我认为最好的方法是使用 Qusai 说的 babel-plugin-remove-decorators(可用于在测试中删除它们),但我编写的预处理器不同,如下所示:

'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;
  }
};

注意 babel.transform 调用,我将 babel-plugin-remove-decorators:before 元素作为数组值传递,请参阅:https://babeljs.io/docs/usage/options/

要将其与 Jest(我使用的是它)连接起来,您可以在 package.json:

中使用如下设置来完成
"jest": {
  "rootDir": "./src",
  "scriptPreprocessor": "../preprocessor.js",
  "unmockedModulePathPatterns": [
    "fbjs",
    "react"
  ]
},

其中 preprocessor.js 是预处理器的名称。

使用 Enzyme 测试高阶/浅层装饰器 使用名为 dive()

的方法

关注此 link,了解潜水的工作原理

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

所以你可以浅化更高阶的组件,然后深入内部。

在上面的例子中:

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

同样,您可以访问属性并对其进行测试。