Как протестировать декорированный компонент React с мелкой визуализацией

Я следую этому руководству: http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/

Попытка узнать, как работает "мелкий рендеринг".

У меня есть компонент более высокого порядка:

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 type - это функция более высокого порядка withMUI.

PlayerProfile, украшенный withMUI, используя мелкую визуализацию, отображается только компонент PlayerProfile, а не он. Так что мелкий рендеринг не будет работать с декорированными компонентами, которые я предполагаю.

Мой вопрос:

Почему в учебнике result.type ожидается div, но в моем случае это не так.

Как я могу протестировать компонент React, украшенный компонентом более высокого порядка, используя мелкий рендеринг?

Ответы

Ответ 1

Вы не можете. Сначала пусть немного обесцвечитель декоратора:

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

withMUI возвращает другой класс, поэтому класс PlayerProfile существует только с закрытием MUI.

Это упрощенная версия:

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 (функция идентификации). Это может вызвать странные побочные эффекты и необходимо сделать со стороны тестирования, поэтому ваши тесты и источник могут выпасть из синхронизации при добавлении декораторов.

Не использование декораторов выглядит как безопасный вариант здесь.

Ответ 2

Используйте фермент для проверки более высокого порядка/декораторов с помощью мелкой с методом под названием dive()

Следуйте по этой ссылке, чтобы увидеть, как работает погружение.

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)

Аналогичным образом вы можете получить доступ к свойствам и протестировать его.

Ответ 3

Вы можете использовать плагин '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

Ответ 4

Я думаю, что приведенный выше пример запутан, потому что концепция decorator используется взаимозаменяемо с идеей "компонента более высокого порядка". Я обычно использую их в комбинации, что облегчит тестирование/перепрошивку/издевку.

Я бы использовал декоратор для:

  • Предоставление реквизитов дочернему компоненту, как правило, для привязки/прослушивания хранилища потоков

Где бы я использовал компонент более высокого порядка

  • чтобы связать контекст более декларативным способом.

Проблема с перекомпоновкой - я не думаю, что вы можете переделать все, что применяется за пределами экспортируемой функции/класса, что имеет место для декоратора.

Если вы хотите использовать комбинацию декораторов и компонентов более высокого порядка, вы можете сделать что-то вроде следующего:

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

Затем, когда вы хотите протестировать, вы можете легко "переустановить" ваши магазины, поставляемые от декоратора, потому что декоратор находится внутри экспортированного компонента более высокого порядка;

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

});

Я уверен, что это действительно не отвечает на ваш первоначальный вопрос, но я думаю, что у вас возникают проблемы при согласовании, когда нужно использовать декораторы против более высоких компонентов заказа.

здесь есть несколько хороших ресурсов:

Ответ 5

В моем случае декораторы очень полезны, и я не хочу избавляться от них (или возвращать завернутые и развернутые версии) в мое приложение.

Лучший способ сделать это, на мой взгляд, - использовать babel-plugin-remove-decorators (который можно использовать для удаления их в тестах), говорит Qusai, но я написал препроцессор по-другому, как показано ниже:

'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, который im передает элемент 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 - имя препроцессора.