WPBrowser scaffold WP-CLI command 02

Mocking the world in Behat while developing a wp-cli command.

I need Composer

The Composer dependency management tool is a centerpiece of my daily development drill; as such it’s globally available on my machine using the composer alias. The wp-cli command package I’m trying to develop aims at streamlining the scaffolding of a plugin or theme wp-browser managed tests easier; on a macro level what the command needs to do when the wp wpb-scaffold plugin-tests command runs is:

  • check that the system has a valid Composer installation or that the user passed a valid Composer path using the --composer option
  • check the --dir argument the user inserted or the current folder
  • check the destination folder for a composer.json file
  • if there is no composer.json file create one from a template else update it
  • run composer update

The command relies heavily on Composer to get the job done so checking for its availability is a paramount first step. I’ve decided not to get around the problem downloading the phar version of Composer somewhere to avoid a probably useless download and to allow users to tap into an already custom configured installation. I’ve gotprestissimo installed and I’d like that to be used when downloading dependencies.

Using Behat to check on Composer

The first challenge proved to be to check on a global or specified composer installation using Behat; I’m not used to it but wanted to get along with the testing practice used for wp-cli commands and try my hand at it. After some time I was able to write and pass the following composer-detection.feature feature:

Feature: Test that composer existence and accessibility is dealt with.

Background:
Given a WP install

Scenario: if passed a wrong --composer argument it should fail.
When I run `wp wpb-scaffold plugin-tests --composer=/some/file.foo`
Then STDERR should contain:
"""
Error: specified Composer path '/some/file.foo' is not a valid Composer executable.
"""

Scenario: if passed a --composer args that's not Composer it should fail
Given the next command is called with the `--composer` parameter
Given the value of the parameter is `some-file.phar` from data
When I run `wp wpb-scaffold plugin-tests`
Then STDERR should contain:
"""
is not a valid Composer executable.
"""

@pathEnv @fakeComposer
Scenario: if global Composer command is not good it should fail
Given the global $PATH var includes the data dir
When I run `wp wpb-scaffold plugin-tests`
Then STDERR should contain:
"""
'composer' (https://getcomposer.org/) command not found or not good.
"""

The first one is simple: I have to pass the --composer option the path to a file that I’m sure does not exist. The second scenario proved to be quite more challenging: I had to add two custom Given steps and update the When I run...one to persist and use some information in the FeatureContext across steps:

file: features/steps/given.php

$steps->Given( '/^the next command is called with the `(.+)` parameter$/', function ( $world, $parameter ) {
$world->variables['parameterName'] = $parameter;

$world->printDebug( "Next command will have the '{$parameter}' parameter set." );

} );

$steps->Given( '/^the value of the parameter is `(.+)` from data$/', function ( $world, $path ) {
if ( empty( $world->variables['parameterName'] ) ) {
throw new \Behat\Behat\Exception\UndefinedException( 'Parameter value is missing' );
}
$path = $world->get_data_dir( $path );

if ( ! file_exists( $path ) ) {
throw new \Behat\Behat\Exception\ErrorException( 0, "File '{$path}' does not exist.", __FILE__, __LINE__ - 3 );
}

$toAppend = ' ' . $world->variables['parameterName'] . '=' . $path;

if ( ! empty( $world->variables['appendParameter'] ) ) {
$world->variables['appendParameter'] .= $toAppend;
} else {
$world->variables['appendParameter'] = $toAppend;
}

$world->printDebug( "Next command will have '{$world->variables['appendParameter']}' appended." );

unset( $world->variables['parameterName'] );
} );

$steps->Given( '/^the global \$PATH var includes the data dir$/', function ( $world ) {
$path = getenv( 'PATH' );
$dataDir = $world->get_data_dir();
$newPath = empty( $path ) ? $dataDir : $dataDir . ':' . $path;

$world->printDebug( "PATH set to '{$newPath}'" );

putenv( 'PATH=' . $newPath );
} );

The key here is that each step will be passed a persisting instance of the FeatureContext in the $world variable; on that instance I’m setting and getting values using the public variables property. See the [full given.php file here](!gghhf wpcli-wpbrowser-tests master “features/steps/given.php”). I’ve added the get_data_dir method to the FeatureContext class to get the path to the feature/_data folder of Codeception custom. See the full ‘FeatureContext’ class here. The When I run... handler was slightly modified to accomodate for possible parameters:

$steps->When( '/^I (run|try) `([^`]+)`$/', function ( $world, $mode, $cmd ) {
$cmd = $world->replace_variables( $cmd );

if ( isset( $world->variables['appendParameter'] ) ) {
$cmd .= $world->variables['appendParameter'];

unset( $world->variables['appendParameter'] );
}
$world->result = invoke_proc( $world->proc( $cmd ), $mode );
} );

See the full when file here.

Mocking PATH in a Behat scenario

I thought I would have had to run for integration testing to cover the scenario where the globally available composer command is, for mysterious reasons, not working but Behat proved me wrong. There are some “gotchas” though. I need to prepend a path to the globally defined PATH that will point to the features/_data folder and in that folder have a composer file that’s not really Composer. Behat allows tagging scenarios and features to tell the feature context something different should happen; notice above I’ve tagged the last scenario @pathEnv @fakeComposer. I’ve implemented support for these two tags in the FeatureContext class to put the required context in place for the scenario:

// file features/bootstrap/FeatureContext.php

/**
* @BeforeScenario @pathEnv
*/
public function backupPathEnvVar() {
$this->pathBackup = getenv( 'PATH' );
}

/**
* @AfterScenario @pathEnv
*/
public function restorePathEnvVar() {
putenv( 'PATH=' . $this->pathBackup );
}

/**
* @BeforeScenario @fakeComposer
*/
public function makeFakeComposerExecutable() {
chmod( $this->get_data_dir( 'composer' ), 111 );
}

What’s happening is:

  • backing up the PATH environment variable
  • prepending the path to the data dir to it
  • restoring PATH to its original value after the scenario ran

The missing piece is now the Given the global $PATH var includes the data dir step:

// file features/steps/given.php

$steps->Given( '/^the global \$PATH var includes the data dir$/', function ( $world ) {
$path = getenv( 'PATH' );
$dataDir = $world->get_data_dir();
$newPath = empty( $path ) ? $dataDir : $dataDir . ':' . $path;

$world->printDebug( "PATH set to '{$newPath}'" );

putenv( 'PATH=' . $newPath );
} );

to have all the scenarios in the feature pass:

Behat Composer path tests passing

On GitHub

As I iterate over the code I push changes to its repository so that’s the place to check for code.