Как протестировать декорированный компонент 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
- имя препроцессора.