Developing a Local addon – 07
October 4, 2017
Step up testing of React components with Enzyme.
Part of a series
This post is the seventh in a series where I'm chronicling my attempt at a Local addon; you can find the other posts here:
- What I'm trying to build
- Understanding Local stack for the task at hand
- Enter React
- Enter testing
- Mocking with Sinon.js
- Introducing Enzyime
The high-level idea is to be able to control the XDebug configuration of the PHP version used to serve the site. This would allow me to activate and deactivate XDebug and set some relevant fields in the process.
As any one of my projects it's a discovery process while trying to "scratch an itch"; along the way, I will make amateur mistakes in all the technological fields involved (Docker, Electron and React in this case) and I know it.
Getting it "done right" has more value for me, in this context, than "getting it done".
I've put a repository online that contains the code in its current state on the master
branch; it has more the release state of a leak than that of an alpha though; the code I show in this post can be found here.
Some refactoring for the purpose of testing
In my previous post I've introduced Enzyme to be able to test React components.
I've written some basic tests for the XDebugControl
component to make sure it would correctly render the status according to some defined initial states.
I've concluded the post where I would have had to put in place at least a test to make sure that, in the case where errors are thrown from lower levels of the add-on application, the UI would gracefully handle those displaying them to the user.
For the time being the two possible sources of errors, in the addon code, are the system/Docker
and system/Container
classes; each could raise a DockerError
or a ContainerError
respectively.
From a discursive point of view what I need to do is to mock both the classes, in some way, and set them up in such a way that any method call will raise the corresponding error.
In the current code implementation I'm instantiating those classes in the XDegubControl::constructor
method itself; this makes the two instances implicit (or "hidden") dependencies, and that in turn makes their mocking more difficult.
class XDebugControl extends Component {
constructor( props ) {
if ( undefined === props.environment ) {
props.environment = context.environment
}
super( props )
this.state = {
siteStatus: 'off',
xdebugStatus: 'n/a yet...',
}
this.site = props.sites[props.params.siteID]
this.docker = new Docker( props.environment, childProcess )
this.container = new Container( this.docker, this.site )
}
}
Was I in PHP my preference would go to refactoring the constructor
method to expose instances of the Docker
and Container
classes as explicit dependencies; something like this:
class XDebugControl extends Component {
constructor( props, docker, container ) {
if ( undefined === props.environment ) {
props.environment = context.environment
}
super( props )
this.state = {
siteStatus: 'off',
xdebugStatus: 'n/a yet...',
}
this.site = props.sites[props.params.siteID]
this.docker = docker
this.container = container
}
}
Since React components are meant to be controlled via their properties, the props
object, I will fall back to use a logic OR
to inject the instances via the properties:
return class XDebugControl extends Component {
constructor( props ) {
if ( undefined === props.environment ) {
props.environment = context.environment
}
super( props )
this.state = {
siteStatus: 'off',
xdebugStatus: 'n/a yet...',
}
this.site = props.sites[props.params.siteID]
this.docker = props.docker || new Docker( props.environment, childProcess )
this.container = props.container || new Container( this.docker, this.site )
}
}
Generating and testing errors
Now that I can inject instances of the Docker
and Container
classes that I control in the XDebugControl
object, I can write a first test for the errors (I've removed the other test methods for readability):
const React = require( 'react' )
const Component = require( 'react' ).Component
const expect = require( 'chai' ).expect
const mount = require( 'enzyme' ).mount
const shallow = require( 'enzyme' ).shallow
const sinon = require( 'sinon' )
const context = {
'React': React,
}
const XDebugControl = require( './../../../src/Component/XDebugControl' )( context )
const Error = require( './../../../src/Component/Error' )( context )
const Container = require( './../../../src/System/Container' )()
const Docker = require( './../../../src/System/Docker' )()
const DockerError = require( './../../../src/Errors/DockerError' )()
const ContainerError = require( './../../../src/Errors/ContainerError' )()
describe( '<XDebugControl/>', function () {
before( function () {
this.props = {
params: {
siteID: 'foo',
},
sites: {
foo: {
environment: 'custom',
container: '123344',
},
},
environment: {
dockerPath: '/app/docker',
dockerEnv: {key: 'value'},
},
siteStatus: 'running',
}
} )
it( 'renders correctly a DockerError', function () {
const docker = sinon.createStubInstance( Docker )
const container = sinon.createStubInstance( Container )
container.getXdebugStatus.throws( function () {
return new DockerError( 'something happened!' )
} )
this.props.docker = docker
this.props.container = container
const wrapper = shallow( <XDebugControl {...this.props}/> )
wrapper.instance().componentWillReceiveProps( this.props )
wrapper.update()
expect( wrapper.find( 'Error' ) ).to.have.length( 1 )
} )
} )
The before
method, similar in functionalities to PhpUnit setUp
or Codeception _before
methods, has not changed; I'm still setting up the basic state needed to initialize the XDebugControl
component correctly.
The test contains some findings worth pointing out, though, so here is a line by line breakdown:
const docker = sinon.createStubInstance( Docker )
const container = sinon.createStubInstance( Container )
Here I "stub" instances of the Docker
and Container
classes; this is a method provided by Sinon that will create a "double" of the class where each method, unless otherwise specified, is a no-op.
The XDebugControl
class will try to fetch the status of the XDebug extension from the container in the componentWillReceiveProps
method.
Since this method is not called in the initial component rendering no method will be called, during the initial rendering, on either stub.
container.getXdebugStatus.throws( function () {
return new ContainerError( 'something happened!' )
} )
I, then, override the container
stub behaviour: when the Container::getXdebugStatus
method is called on the container
stub, then it should generate a ContainerError
.
this.props.docker = docker
this.props.container = container
Now that the stubs are correctly set up I prepare their injection in the XDebugControl
component by adding them to the properties that will be used to build the XDebugControl
component.
const wrapper = shallow( <XDebugControl {...this.props}/> )
The XDebugControl
is rendered in shallow mode, the only methods called on the object are constructor
and render
, nothing has still happened here.
wrapper.instance().componentWillReceiveProps( this.props )
Now the wrapper
object, an Enzyme facility roughly representing the DOM, contains a fully constructed instance of the XDebugControl
component.
I get hold of that instance using wrapper.instance()
and call the componentWillReceiveProps
method on it passing the properties to it.
The XDebugControl
instance will call, in turn, the getXDebugStatus
method on the container
stub and this will generate an error that the component should handle.
wrapper.update()
While the XDebugControl
instance has done something the virtual DOM has not been updated and it still contains the HTML produced by the previous shallow rendering.
The call to wrapper.update()
method will replace the previous virtual DOM with the new one.
expect( wrapper.find( 'Error' ) ).to.have.length( 1 )
Finally, I make an assertion checking for an <Error/>
element rendered on the page.
I look for an <Error />
component as I've modified the XDebugControl::render
method to this:
class XDebugControl extends Component {
constructor( props ) {
if ( undefined === props.environment ) {
props.environment = context.environment
}
super( props )
this.state = {
siteStatus: 'off',
xdebugStatus: 'n/a yet...',
error: null,
}
this.site = props.sites[props.params.siteID]
this.docker = props.docker || new Docker( props.environment, childProcess )
this.container = props.container || new Container( this.docker, this.site )
}
componentWillReceiveProps( nextProps ) {
let newState = null
if ( nextProps.siteStatus === 'running' ) {
try {
newState = {
siteStatus: 'running',
xdebugStatus: this.container.getXdebugStatus(),
error: null,
}
}
catch ( e ) {
if ( e.name === 'DockerError' || e.name === 'ContainerError' ) {
newState = {
error: {
source: e.name === 'DockerError' ? 'Docker' : 'Container',
message: e.message,
},
}
} else {
throw e
}
}
} else {
// ...
}
this.setState( newState )
}
render() {
let statusString = null
let error = null
let button = null
let fieldList = null
let isCustom = this.site.environment === 'custom'
let xdebugStatus = null
let statusStyle = {'text-transform': 'uppercase'}
let isRunning = this.props.siteStatus === 'running'
if ( this.state.error !== null ) {
error = (
<Error {...this.state.error}/>
)
} else {
// ...
}
const titleStyle = {margin: '.25em auto', 'font-size': '125%'}
return (
<div style={{display: 'flex', flexDirection: 'column', flex: 1, padding: '0 5%'}}>
<h3>XDebug Controls</h3>
<span className='XdebugStatus' style={titleStyle}>{statusString}</span>
{error}
{button}
{fieldList}
</div>
)
}
}
But why look for an Error
component and not for its content?
The Error component
While the style and structure of the Error
component might vary this is the current content of the src/Component/Error.js
file:
module.exports = function ( context ) {
const React = context.React
return function Error( props ) {
return (
<section className='Error'>
<p className='Error__Message'>
From <span className='Error__Message__Source'>{props.source}</span>
-
<span className='Error__Message__Text'>{props.message}</span>
</p>
</section>
)
}
}
It's a functional (or stateless) component that will render some HTML code using the provided properties.
While there is nothing really new about this it took me a while to understand why the test above, written like this, is failing:
it( 'renders correctly a DockerError', function () {
const docker = sinon.createStubInstance( Docker )
const container = sinon.createStubInstance( Container )
container.getXdebugStatus.throws( function () {
return new DockerError( 'something happened!' )
} )
this.props.docker = docker
this.props.container = container
const wrapper = shallow( <XDebugControl {...this.props}/> )
wrapper.instance().componentWillReceiveProps( this.props )
wrapper.update()
expect( wrapper.find( '.Error__Message__Source' ).text() ).to.equal('Docker')
expect( wrapper.find( '.Error__Message__Message' ).text() ).to.equal('something happened!')
} )
[](https://theaveragedev.com/wp-content/uploads/2017/10/failing-mocha-test.png)
While the test, written the way I've shown above, passes:
[](https://theaveragedev.com/wp-content/uploads/2017/10/passing-mocha-tests.png)
The reason being that I'm shallow rendering.
This means components will be rendered one level deep, or, put another way, nested components will not be rendered. The Error
component is, in the context of the XDebugControl
component, a nested element and will be shallow rendered to, literally, <Error />
.
But where am I, then, testing the Error
component? In the test/render/Component/Error.spec.js
file dedicated to it:
const React = require( 'react' )
const expect = require( 'chai' ).expect
const mount = require( 'enzyme' ).mount
const shallow = require( 'enzyme' ).shallow
const context = {
'React': React,
}
const Error = require( './../../../src/Component/Error' )( context )
describe( '<Error/>', function () {
it( 'renders correctly when provided all props', function () {
const props = {source: 'foo', message: 'bar'}
const wrapper = mount( <Error {...props}/> )
expect( wrapper.find( '.Error__Message__Source' ).text() ).to.equal( 'foo' )
expect( wrapper.find( '.Error__Message__Text' ).text() ).to.equal( 'bar' )
} )
it( 'renders the source as unknown if not defined', function () {
const props = {message: 'bar'}
const wrapper = mount( <Error {...props}/> )
expect( wrapper.find( '.Error__Message__Source' ).text() ).to.equal( 'Unknown' )
} )
it( 'renders the message as not provided if not defined', function () {
const props = {source: 'bar'}
const wrapper = mount( <Error {...props}/> )
expect( wrapper.find( '.Error__Message__Text' ).text() ).to.equal( 'no message provided' )
} )
} )
Being a functional component testing it is way easier.
Next
I've pretty much satisfied my hunger for new testing knowledge and it's time to wrap the addon and release the first version.
The code shown in this post is tagged step-7
on GitHub.