Developing a Local addon – 07

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:

  1. What I’m trying to build
  2. Understanding Local stack for the task at hand
  3. Enter React
  4. Enter testing
  5. Mocking with Sinon.js
  6. 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!')
} )

While the test, written the way I’ve shown above, passes:

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.