Je suis ce tutoriel: http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/
Essayer d'apprendre comment le "rendu superficiel" fonctionne.
J'ai un composant d'ordre supérieur:
import React from 'react';
function withMUI(ComposedComponent) {
return class withMUI {
render() {
return <ComposedComponent {...this.props}/>;
}
};
}
et un composant:
@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>
);
}
}
et un test:
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);
});
});
La variable result
contient this.renderer.getRenderOutput()
Dans le tutoriel, le result.type
est testé comme suit:
expect(result.type).toEqual('div');
dans mon cas, si je me connecte le result
c'est:
LOG: Object{type: function PlayerProfile() {..}, .. }
alors j'ai changé mon test comme:
expect(result.type).toEqual(PlayerProfile)
maintenant cela me donne cette erreur:
Assertion Error: expected [Function: PlayerProfile] to equal [Function: withMUI]
Donc, le type de PlayerProfile
est la fonction d'ordre supérieur withMUI
.
PlayerProfile
décoré avec withMUI
, avec un rendu peu profond, seul le composant PlayerProfile
est rendu et non ses enfants. Un rendu aussi superficiel ne fonctionnerait pas avec des composants décorés, je suppose.
Ma question est:
Pourquoi, dans le tutoriel, result.type
devrait être un div, mais dans mon cas, ce n’est pas le cas.
Comment puis-je tester un composant React décoré avec un composant d'ordre supérieur en utilisant un rendu peu profond?
Tu ne peux pas. D'abord, décontrons légèrement le décorateur:
let PlayerProfile = withMUI(
class PlayerProfile extends React.Component {
// ...
}
);
withMUI renvoie une classe différente, de sorte que la classe PlayerProfile n'existe que dans la fermeture de withMUI.
Voici une version simplifiée:
var withMUI = function(arg){ return null };
var PlayerProfile = withMUI({functionIWantToTest: ...});
Vous transmettez la valeur à la fonction, elle ne la restitue pas, vous n'avez pas la valeur.
La solution? Tenez une référence à cela.
// no decorator here
class PlayerProfile extends React.Component {
// ...
}
Ensuite, nous pouvons exporter les versions encapsulées et non encapsulées du composant:
// this must be after the class is declared, unfortunately
export default withMUI(PlayerProfile);
export let undecorated = PlayerProfile;
Le code normal utilisant ce composant ne change pas, mais vos tests utiliseront ceci:
import {undecorated as PlayerProfile} from '../src/PlayerProfile';
L'alternative consiste à simuler la fonction withMUI comme étant (x) => x
(la fonction d'identité). Cela peut entraîner des effets secondaires étranges et doit être effectué du côté des tests. Par conséquent, vos tests et votre source risquent de ne plus être synchronisés à mesure que des décorateurs sont ajoutés.
Ne pas utiliser de décorateurs semble être l'option sûre ici.
Utilisez Enzyme pour tester les décorateurs supérieurs avec Shallow Avec une méthode appelée dive ()
Suivez ce lien pour voir comment fonctionne la plongée
https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/dive.md
Vous pouvez donc plonger le composant dans un ordre peu élevé, puis plonger à l'intérieur.
const wrapper=shallow(<PlayerProfile name={name} avatar={}/>)
expect(wrapper.find("PlayerProfile").dive().find(".player-profile").length).toBe(1)
De même, vous pouvez accéder aux propriétés et les tester.
Vous pouvez utiliser le plugin 'babel-plugin-remove-decorators'. Cette solution vous permettra d'écrire normalement vos composants sans exporter les composants décorés et non décorés.
Installez d'abord le plug-in, puis créez un fichier avec le contenu suivant, appelons-le 'babelTestingHook.js'
require('babel/register')({
'stage': 2,
'optional': [
'es7.classProperties',
'es7.decorators',
// or Whatever configs you have
.....
],
'plugins': ['babel-plugin-remove-decorators:before']
});
et exécuter vos tests comme ci-dessous ignorera les décorateurs et vous pourrez tester les composants normalement
mocha ./tests/**/*.spec.js --require ./babelTestingHook.js --recursive
Je pense que l'exemple ci-dessus est source de confusion car le concept decorator
est utilisé de manière interchangeable avec l'idée d'un "composant d'ordre supérieur". Je les utilise généralement en combinaison, ce qui facilite les tests/recâblage/moquages.
J'utiliserais décorateur pour:
Où comme j'utiliserais un composant d'ordre supérieur
Le problème avec le recâblage est que je ne pense pas que vous puissiez recâbler quoi que ce soit qui est appliqué en dehors de la fonction/classe exportée, comme dans le cas d'un décorateur.
Si vous souhaitez utiliser une combinaison de décorateurs et de composants d'ordre supérieur, vous pouvez procéder comme suit:
//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);
Ensuite, lorsque vous souhaitez tester, vous pouvez facilement "recâbler" vos magasins fournis par le décorateur, car celui-ci se trouve à l'intérieur du composant de commande supérieure exporté.
//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);
});
});
Je suis à peu près sûr que cela ne répond pas vraiment à votre question initiale, mais je pense que vous avez du mal à concilier le moment d'utilisation des décorateurs par rapport aux composants d'ordre supérieur.
quelques bonnes ressources sont ici:
Dans mon cas, les décorateurs sont très utiles et je ne veux pas m'en débarrasser (ni renvoyer les versions emballées et non emballées) dans mon application.
La meilleure façon de faire cela, à mon avis, est d'utiliser le babel-plugin-remove-decorators
(qui peut être utilisé pour les supprimer dans les tests), a dit Qusai, mais j'ai écrit le pré-processeur différemment, comme ci-dessous:
'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;
}
};
Prenez note de l'appel babel.transform
qui im passage l'élément babel-plugin-remove-decorators:before
en tant que valeur de tableau, voir: https://babeljs.io/docs/usage/options/
Pour raccorder cela avec Jest (ce que j’ai utilisé), vous pouvez le faire avec les paramètres comme ci-dessous dans votre package.json
:
"jest": {
"rootDir": "./src",
"scriptPreprocessor": "../preprocessor.js",
"unmockedModulePathPatterns": [
"fbjs",
"react"
]
},
Où preprocessor.js
est le nom du préprocesseur.