Developing a Local addon – 04
September 23, 2017
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.
[](https://theaveragedev.com/wp-content/uploads/2017/09/activate-deactivate.gif)
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:
[](https://theaveragedev.com/wp-content/uploads/2017/09/mocha-no-tests.png)
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:
[](https://theaveragedev.com/wp-content/uploads/2017/09/mocha-works.png)
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:
[](https://theaveragedev.com/wp-content/uploads/2017/09/mocha-container-test-error.png)
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:
[](https://theaveragedev.com/wp-content/uploads/2017/09/mocha-passing.png)
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.