Developing a Local addon – 04

Enter testing.

This is the fourth part

This post is the fourth one, following a first, a second and a third one, in a series where I’m chronicling my attempt at a Local addon.
The high-level idea is to be able to control the XDebug configuration of the PHP version used to serve the site allowing me to activate and deactivate XDebug and setting some relevant fields in the process; in the previous post I’ve exposed my understanding of Local technology stack and how to use it to make it do, through “raw” terminal commands, what I want it to do.
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 technologic fields involved (Docker 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 current state

After putting together a first working prototype of a Local add-on to test the idea, the code you can currently find on the repository, I went back to the code to try and rewrite it in a way that would satisfy my taste for code and need to learn new things.
This post starts from a current state of the code that simply displays the state of the XDebug extension on the site and allows activating and deactivating the extension on it.

Choosing the testing subjects

Since I’m taking the time to write code that I feel as “pleasing to me”, a totally subjective perception, of course, it’s due time I roll into the cake testing: that might be the reason I’m doing this as I tend to like testing things.
As a “PHP guy”, my JavaScript skill is passable but my knowledge of JavaScript testing tools is lacking; I know codecept.js exists and seems to be DOM testing oriented, I know some use the mocha.js testing framework and I’m sure much more exist.
In an approach typical to a newbie I will use the tools I’ve found more documentation about, but before I choose the tools it’s worth defining what I want to test and how.
My Local addon has an entry point in the lib/renderer.js file, in that file some hooking is done, in a WordPress fashion, and the main component of the addon, lig/Components/XDebugControl.js, is delegated the responsibility of rendering and managing the UI living under the “XDebug Control” tab.
The component has a number of dependencies that can be roughly divided into context, global variables and objects, other components, and support classes like lib/System/Docker.js and lib/System/Container.js.
Using the same mentality I would use to approach a PHP project, with Codeception I would try to write an acceptance test first; sadly my knowledge of the Electron application and React stack is so meager that enterprise alone would swallow me whole.
I will start, instead, from the lowest level of testing and try to write a unit test first as it seems more approachable.
As a first step I will try to test the src/System/Container.js class; the class depends on the context and the Docker class and has a wealth of methods to cover.

Setting up the testing tools

I’ve decided, for my first JavaScript testing steps, on mocha as a testing framework and on chai as an assertion framework.
To make a rough comparison with the PHP reality I know Mocha is to JavaScript what PHPUnit or Codeception is to PHP and Chai is to JavaScript tests what the PHPUnit PHPUnit\Framework\Assert class is to PHP.
Installing the tools is quite easy:

npm install --save mocha chai

The mocha binary lives at ./node_modules/mocha/bin/mocha, in relation to the project root folder, so launching it will require this command:

./node_modules/mocha/bin/mocha

Since there are no tests Mocha will complain:

I want to keep my tests organized and, being aware of my tendency to over-engineer stuff, I’ve created my first test in the test/unit/System/Container.spec.js file; by default, Mocha will look for tests in the /test folder but I’ve added a test/mocha.opts file to customize that behavior:

test/unit/**/*.spec.js
--recursive

Mocha will read the above as “run any file ending in .spec.js living in the test/unit folder or a sub-folder”; to make sure everything is working I write a first idiotic test to the /test/unit/System/Container.spec.js:

const expect = require( 'chai' ).expect

describe( 'mocha should work', function () {
    it( 'works!', function () {
        expect( true ).to.be.true
    } )
} )

Running the tests again confirms Mocha works:

To be true the test also confirms that chai is working: it’s, in fact, the chai library that provides the “ablative” expect... syntax.

Writing the first real test

It’s now time to write the first test and I will try to test the only method that’s currently being used on the Container class: getXdebugStatus(); the method will internally trigger a call to the Container::exec() method though so the latter might prove a better starting point.
The Container class current code is this one (I’ve not relevant code for the sake of brevity):

module.exports = function ( context ) {
    const Docker = require( './Docker' )( context )

    return class Container {
        constructor( docker, site ) {
            this.docker = docker
            this.site = site
            this.sitePhpBin = undefined
            this.sitePhpIniFile = undefined
            this.sitePhpVersion = undefined
            this.restartCommandMap = {
                'apache': {
                    '5.2.4': `service apache2 restart`,
                    '5.2.17': `service apache2 restart`,
                    '5.3.29': `service php-5.3.29-fpm restart`,
                    '5.4.45': `service php-5.4.45-fpm restart`,
                    '5.5.38': `service php-5.5.38-fpm restart`,
                    '5.6.20': `service php-5.6.20-fpm restart`,
                    '7.0.3': `service php-7.0.3-fpm restart`,
                    '7.1.4': `service php-7.1.4-fpm restart`,
                },
                'nginx': {
                    '5.2.4': `service php-5.2.4-fpm restart`,
                    '5.2.17': `service php-5.2.17-fpm restart`,
                    '5.3.29': `service php-5.3.29-fpm restart`,
                    '5.4.45': `service php-5.4.45-fpm restart`,
                    '5.5.38': `service php-5.5.38-fpm restart`,
                    '5.6.20': `service php-5.6.20-fpm restart`,
                    '7.0.3': `service php-7.0.3-fpm restart`,
                    '7.1.4': `service php-7.1.4-fpm restart`,
                },
            }
        }

        exec( command ) {
            let fullCommand = `exec -i ${this.site.container} sh -c "${command}"`

            return Docker.runCommand( fullCommand )
        }

        getXdebugStatus() {
            try {
                let status = this.exec( `wget -qO- localhost/local-phpinfo.php | grep Xdebug` )
                if ( status.length !== 0 ) {
                    return 'active'
                }
                return 'inactive'
            }
            catch ( e ) {
                return 'inactive'
            }
        }
    }
}

The Container.exec command uses two dependencies:
1. the site property, set from the context global in the constructor method
2. the Docker class, required at the start of the file and defined the lib/System/Docker.js file

The first test I want to write will make sure the exec function will return whatever the Docker::runCommand method will return:

// file test/unit/System/Container.js

const expect = require( 'chai' ).expect

describe( 'exec', function () {
    it( 'returns what Docker::runCommand returns', function () {
        const dockerMock = {
            runCommand: function () {
                return 'bar'
            },
        }
        const siteMock = {
            container: 'foo-container',
        }
        const context = {
            docker: dockerMock,
            site: siteMock,
        }

        const Container = require( './../../../src/System/Container' )( context )
        const container = new Container( dockerMock, siteMock )

        expect( container.exec( 'some-command' ) ).to.be.equal( 'bar' )
    } )
} )

The test yields, but, an error:

The dockerPath variable is not used in the Docker::runCommand method:

module.exports = function ( context ) {
    const childProcess = require( 'child_process' )

    return class Docker {
        static getDockerPath() {
            return context.environment.dockerPath.replace( / /g, '\\ ' )
        }

        static runCommand( command ) {
            let dockerPath = Docker.getDockerPath()
            let fullCommand = `${dockerPath} ${command}`

            return childProcess.execSync( fullCommand, {env: context.environment.dockerEnv} ).toString().trim()
        }
    }
}

While I’m, supposedly, mocking the docker object my current implementation of the Docker::runCommand method is static; exactly like in PHP this means the code becomes a little less testable and while I know, were my knowledge and skills in JavaScript testing deeper, I could get around this, for the time being, I will refactor the Docker class to be instance based.

Refactoring and rewriting the test

As a first step I’ve rewritten the Docker class:

module.exports = function () {
    return class Docker {
        constructor( context, childProcess ) {
            this.dockerPath = context.environment.dockerPath
            this.dockerEnv = context.environment.dockerEnv
            this.childProcess = childProcess
        }

        getDockerPath() {
            return this.dockerPath.replace( / /g, '\\ ' )
        }

        runCommand( command ) {
            let dockerPath = Docker.getDockerPath()
            let fullCommand = `${dockerPath} ${command}`

            return this.childProcess.execSync( fullCommand, {env: this.dockerEnv} ).toString().trim()
        }
    }
}

As a second step I’ve updated the Container class:

module.exports = function () {
    return class Container {
        constructor( docker, site ) {
            this.docker = docker
            this.site = site
            this.sitePhpBin = undefined
            this.sitePhpIniFile = undefined
            this.sitePhpVersion = undefined
            this.restartCommandMap = {
                'apache': {
                    '5.2.4': `service apache2 restart`,
                    '5.2.17': `service apache2 restart`,
                    '5.3.29': `service php-5.3.29-fpm restart`,
                    '5.4.45': `service php-5.4.45-fpm restart`,
                    '5.5.38': `service php-5.5.38-fpm restart`,
                    '5.6.20': `service php-5.6.20-fpm restart`,
                    '7.0.3': `service php-7.0.3-fpm restart`,
                    '7.1.4': `service php-7.1.4-fpm restart`,
                },
                'nginx': {
                    '5.2.4': `service php-5.2.4-fpm restart`,
                    '5.2.17': `service php-5.2.17-fpm restart`,
                    '5.3.29': `service php-5.3.29-fpm restart`,
                    '5.4.45': `service php-5.4.45-fpm restart`,
                    '5.5.38': `service php-5.5.38-fpm restart`,
                    '5.6.20': `service php-5.6.20-fpm restart`,
                    '7.0.3': `service php-7.0.3-fpm restart`,
                    '7.1.4': `service php-7.1.4-fpm restart`,
                },
            }
        }

        exec( command ) {
            let fullCommand = `exec -i ${this.site.container} sh -c "${command}"`

            return this.docker.runCommand( fullCommand )
        }

        getXdebugStatus() {
            try {
                let status = this.exec( `wget -qO- localhost/local-phpinfo.php | grep Xdebug` )
                if ( status.length !== 0 ) {
                    return 'active'
                }
                return 'inactive'
            }
            catch ( e ) {
                return 'inactive'
            }
        }
    }
}

And finally the specification file:

const expect = require( 'chai' ).expect
const Container = require( './../../../src/System/Container' )()

describe( 'exec', function () {
    it( 'returns what Docker::runCommand returns', function () {
        const dockerMock = {
            runCommand: function () {
                return 'bar'
            },
        }
        const siteMock = {
            container: 'foo-container',
        }

        const container = new Container( dockerMock, siteMock )

        expect( container.exec('some-command') ).to.be.equal( 'bar' )
    } )
} )

The test is now passing:

Next

The code shown in this post can be found on GitHub tagged step-4.
I will extend the testing to the remaining classes and methods, where needed, and introduce the use and objectives of the sinon.js library for testing purposes.

I appreciate your input