React Testing
Workflow and strategies
Who am I
Vladimir Novick
Front End Developer & Architect
mail: vnovick@gmail.com
twitter: @VladimirNovick
github: vnovick
facebook: vnovickdev
React
Why bother?
Why TDD?
Eliminate fear of change
Agenda
-
Setup your test environment
-
Strategies to test React Component
-
Simulating events
-
Overriding module inner dependencies
-
Flux & Redux testing strategies
-
TDD Workflow
Assumptions
-
ES6 Basic Syntax
-
Client Side testing framework (mocha/jasmine)
-
Karma
-
Module bundler (webpack/browserify)
The Setup
webpack + mocha + karma
var webpackConfig = require('./frontend/webpack.config.js');
var path = require("path");
module.exports = function(config) {
config.set({
basePath: __dirname,
resolve: {
root: [
path.join(__dirname, 'frontend'),
],
},
frameworks: ['mocha-debug', 'mocha'],
files: [
'frontend/scripts/specs/test.js'
],
exclude: [
],
plugins: [
'karma-chrome-launcher',
'karma-mocha',
'karma-sourcemap-loader',
'karma-webpack',
'karma-mocha-debug'
],
preprocessors: {
'frontend/scripts/specs/test.js': [ 'webpack', 'sourcemap' ]
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
webpack: webpackConfig
});
};
Assertion libs
Advised by Rackt team: https://github.com/mjackson/expect
import React from 'react';
import expect from 'expect';
import expectJSX from 'expect-jsx';
expect.extend(expectJSX);
class TestComponent extends React.Component {}
describe('expect-jsx', () => {
it('works', () => {
expect(<div />).toEqualJSX(<div />);
// ok
expect(<div a="1" b="2" />).toEqualJSX(<div />);
// Error: Expected '<div\n a="1"\n b="2"\n/>' to equal '<div />'
expect(<span />).toNotEqualJSX(<div/>);
// ok
expect(<div><TestComponent /></div>).toIncludeJSX(<TestComponent />);
// ok
});
});
is extended with expect-jsx
Testing React Component
There are different techniques to test React Component with react-addons-test-utils
React TestUtils
Provides set of useful functions to check if React elements are rendered correctly
import { renderIntoDocument, isCompositeComponentWithType, isElementOfType } from 'react-addons-test-utils';
import { assert } from 'chai';
var HelloMessage = React.createClass({
render: function() {
return <div><Title text={this.props.name}/></div>;
}
});
const Title = ({ text }) => (<h1 className="title">{text}</h1>)
describe('Hello Message', () => {
it('Hello Message renders as composite component of type HelloMessage, () => {
const HelloComponent = renderIntoDocument(<HelloMessage name="testName"/>);
assert.equal(isCompositeComponentWithType(HelloComponent, HelloMessage), true)
});
it('Hello Message instance is created properly, () => {
assert.equal(isElementOfType(<HelloMessage/>, HelloMessage), true)
});
});
-
renderIntoDocument
-
mockComponent
-
isElement
-
isElementOfType
-
isCompositeComponent
-
scryRenderedDOMComponentsWithClass
-
findRenderedDOMComponentWithClass
Useful Methods
Simulating events
React test utils provide Simulate method to simulate every event React understands
import { Simulate, findRenderedDOMComponentWithClass, renderIntoDocument } from 'react-addons-test-utils'
let HelloMessage = React.createClass({
render: function() {
return <div>
<Title clickCallback={()=>{ console.log("Click")}}/>
</div>;
}
});
let Title = ({clickCallback}) => <div className="titleComponent"
onClick={clickCallback}>
Title with click callback
</div>
let helloComponent = renderIntoDocument(<HelloMessage/>);
Simulate.click(
findRenderedDOMComponentWithClass(helloComponent, 'titleComponent')
);
Shallow Rendering
Shallow rendering is our current recommendation.
It allows you to instantiate a component and get the result of its render function (React Element object).
At that point you can inspect its type and props to verify that the correct result was rendered
Ben Alpert - React team
Asserting render output
Shallow Renderer will render only one level deep meaning you don't need to worry about behavior of child components
import { createRenderer } from 'react-addons-test-utils';
import { assert } from 'chai';
var HelloMessage = React.createClass({
render: function() {
return <div><Title text={this.props.name}/></div>;
}
});
const Title = ({ text }) => (<h1 className="title">{text}</h1>)
describe('Hello Message - check for children type', () => {
it('Hello Message renders children of type Title, () => {
const shallowRenderer = createRenderer();
shallowRenderer.render(<HelloMessage name="Vladimir"/>);
assert.equal(
isElementOfType(
shallowRenderer.getRenderOutput().props.children.type,
Title
),
true);
});
});
Extending ShallowRendering with skin-deep
skin-deep wraps shallowRender with some useful extras to access instance methods
import React from 'react';
import sd from 'skin-deep';
const tree = sd.shallowRender(React.createElement(MyComponent, {}));
const instance = tree.getMountedInstance();
const vdom = tree.getRenderOutput();
Check against expected children
import React from 'react';
import sd from 'skin-deep';
let HelloMessage = ({ name }) =>
<div>
<Title text={name}/>
<Title2/>
</div>;
let Title = ({ text }) => <h1 className="title">{text}</h1>
let Title2 = _ => <div>Dummy Test</div>;
let children = [
<Title text="Vladimir"/>,
<Title2/>
]
describe('Hello Message - check for children type', () => {
it('Hello Message renders children of type Title, () => {
const tree = sd.shallowRender(<HelloMessage name="Vladimir"/>);
const vdom = tree.getRenderOutput();
expect(vdom.props.children).to.deep.equal(children);
});
});
Isolating and testing instance methods
Skin deep wraps gives us .getMountedInstance method which lets us access instance methods and test them in isolation
Overriding Inner Dependencies
import axios from 'axios';
import ChildComponent from 'childComponent';
export default class MyFancyWrapperComponent extends React.Component {
componentWillMount(){
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (response) {
console.log(response);
});
}
render() {
return (<div className="wrapper-style">
<ChildComponent {...this.props} />
</div>);
}
}
Problem:
Inner module dependencies are private for a module and cannot be accessed via test
Solution
babel-plugin-rewire
A Babel plugin that adds the ability to rewire module dependencies.
{test: /src\/js\/.+\.js$/, loader: 'babel-loader?plugins=rewire' }
Webpack setup
rewire plugin is based on rewire.js but is itegrated into babel as transformer to support ES6 module syntax
Overriding dependencies
import ComponentToTest from 'my-fancy-wrapper-component-module';
function mockAjax () {
//...
return new Promise(...);
}
ComponentToTest.__Rewire__('axios', mockAjax );
//....Assertion is here
ComponentToTest.__ResetDependency__('axios');
Also supports top level variables and require('myModule') overriding in the same manner
__RewireAPI__
You can rewire module dependencies without importing a module
import { __RewireAPI__ as componentRewireAPI } from 'my-fancy-wrapper-component-module';
function mockAjax () {
//...
return new Promise(...);
}
componentRewireAPI.__Rewire__('axios', mockAjax );
//....Assertion is here
componentRewireAPI.__ResetDependency__('axios');
Flux Testing strategies
- Try to avoid putting logic in store constructor
- Export only store class and move store creation logic to separate module
- Try to create store functions as thin and independent as possible (easier to test)
- Override Dispatcher.register callback to test store methods
- Override Dispatcher.dispatch to independently test actions
- Simulate events and test component shallow rendered props for E2E testing
Flux Testing strategies (part2)
Redux testing strategies
Almost everything is pure functions so the testing is even simpler. Less inner dependencies
Basic TDD workflow
- Create Component Test
- Test if component renders successfully
- Write Component class
Components Creation part 1
Basic TDD workflow
- Decide which dumb component it will have
- Write test to check if it renders these components
- Write empty dumb components to pass test
Components Creation part 2
Basic TDD workflow
- test if initialState is passed to component
- flux(Create Store for component)
- redux(create reducer for component)
- connect Store/Reducer to component to pass the test
Store/Reducer connection
Basic TDD workflow
- test if ActionCreators return expected payload
- write ActionCreators to pass test
Action Creators
Basic TDD workflow
- test if Simulating events triggers dispatch
- wire your component events to ActionCreators
Handling Events
Basic TDD workflow
- test if Store/Reducer state is changed based on dispatch
- write Store/Reducer methods to pass tests
Testing Store/Reducer
Keep
Calm
And
TDD
We are hiring!
react@90min.com
React Testing
By vladimirnovick
React Testing
- 2,150