Experiments in test-driven development of a Gutenberg block, part 2
December 16, 2018
This post is the second of a series, read the first post here. The code for this post can be found on GitHub if you want to follow along while reading it.
Conventions
I'm running any terminal command I show in the post, unless otherwise noted, from within the VVV virtual machine, from the root folder or the plugin.
To log into the virtual machine I change directory to the ~/vagrant-local
folder on my laptop and run vagrant ssh
.
Once I'm inside the box and am presented with the prompt I change directory to the plugin root folder:
cd /srv/www/wordpress-one/public_html/wp-content/plugins/reading-time-blocks
Writing the first feature
I've set up my test environment as detailed in the first post, and it's now time to write the first real test to drive my development.
I start, as I usually do, with an acceptance test to provide me with an "umbrella" under which I can modify the code, at this stage still in draft version, to make it behave as intended first, and implement it as I want second.
I've removed the first acceptance test I've created, the one whose only purpose was to confirm the setup was correctly working, and created a new feature to drive my behavior-driven development (BDD) and write as little code as possible to make it work as intended.
From the plugin root folder I run:
codecept g:feature acceptance "Base estimated remaining reading time block"
With this command, I've created a feature file in the tests/acceptance/Base estimated remaining reading time block.feature
file.
This file, with the .feature
extension is a Gherkin syntax file as those used by other BDD frameworks like Cucumber or Behat; Gherkin is a structured file written in plain language that, and I'm doing do that below, can be translated to test code.
The "philosophy" advantage of Gherkin is that its format enforces thinking about behavior in place of implementation; I find this, on a high-level, an excellent way to drive my development.
After filling in the "blanks" of the feature file I've ended up with this:
Feature: Base estimated remaining reading time block
In order to show readers the estimated remaining reading time
As a post editor
I need to be able to drop the estimated reading time block anywhere on the page
Scenario: when inserted at the start of the post the block shows the est. remaining reading time for the whole post
"Steps" should appear in the "Scenario" section; since there are currently none running the feature yields a success:
Writing the feature steps
Adding steps to a feature usually involves, at least, two phases:
- write the step in plain language
- if not already implemented implement the step code I've yet to write anything, so I have to do both, at least initially, for all the steps I'm adding.
I've updated the feature text to this:
Feature: Base estimated remaining reading time block
In order to show readers the estimated remaining reading time
As a post editor
I need to be able to drop the estimated reading time block anywhere on the page
Scenario: when inserted at the start of the post the block shows the est. remaining reading time for the whole post
Given a post of "400" words
And the "tad/reading-time" block is placed at the start of the post
When I see the post on the frontend
Then I should see the block shows an estimated reading time of "about 2 minutes"
When I rerun the test Codeception lets me know I've not implemented the steps yet:
To "translate" the steps into the code I can leverage Codeception built-in generator to kickstart my implementation:
codecept gherkin:snippets acceptance
I can copy and paste the code in the tests/_support/AcceptanceTester.php
as they are but that would lead, as I add more and more steps, to a monster class with far too many responsibilities.
Organizing the steps
To keep the code clean and organized I've created a tests/_support/Traits/Gherkin/
folder and, in there, I've created two traits to start with:
tests/_support/Traits/Gherkin/Editor
- this trait implements all the steps dealing with the backend editor UI; e.g., adding and removing blocks, entering content, saving the post and more.tests/_support/Traits/Gherkin/Frontend
- this trait implements all the steps dealing with the site front-end and UI like navigating to the single post page or making assertions on the front-end version of a post.
I've also modified the names, slugs and icon of the block to remove the example placeholders created by the create-guten-block
scaffold operation and rebuilt the assets using npm run build
; currently the plugin is registering one editor block with the tad/reading-time
namespace and slug.
I mention this here as I'm using the slug to add the block during the tests.
I've modified the composer.json
file to autoload classes in the Test
namespace in the context of development (in the autoload-dev
section):
{
"name": "lucatume/reading-time-blocks",
"description": "Gutenberg blocks to display a post reading time.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Luca Tumedei",
"email": "luca@theaveragedev.com"
}
],
"minimum-stability": "stable",
"require": {},
"require-dev": {
"lucatume/wp-browser": "^2.2"
},
"autoload-dev": {
"psr-4": {
"Test\\": "tests/_support/"
}
}
}
Finally I add use the two new traits in the AcceptanceTester
class:
use Test\Traits\Gherkin\Editor;
use Test\Traits\Gherkin\Frontend;
class AcceptanceTester extends \Codeception\Actor {
use _generated\AcceptanceTesterActions;
use Editor;
use Frontend;
/**
* Traits will store in this associative arrays all relevant information generated or used in test steps.
*
* @var array
*/
protected $data = [];
}
The Frontend trait
I start the translation of plain language into code from the Test\Gherkin\Frontend
trait as that is the easiest one.
Since the acceptance
suite is using the WPWebDriver
module I leverage the methods provided by it to implement the steps:
/**
* Implements all the steps dealing with the site front-end and UI; e.g., navigating to the single post page or making
* assertions on the front-end version of a post.
*
* @package Test\Gherkin
*/
namespace Test\Traits\Gherkin;
/**
* Trait Frontend
*
* @package Test\Gherkin
*/
trait Frontend {
/**
* @When I see the post on the frontend
*/
public function iSeeThePostOnTheFrontend() {
// Use the `post_id` previously saved in the shared data array.
$this->amOnPage( '/index.php?p=' . $this->data['postId'] );
}
/**
* @Then I should see the block shows an estimated reading time of :readingTimeEstimation
*/
public function iShouldSeeTheBlockShowsAnEstimatedReadingTimeOfMinutes( $readingTimeEstimation ) {
$this->see( $readingTimeEstimation );
}
}
The WPWebDriver
from wp-browser is an extension of the WebDriver
one from Codeception and inherits its methods. Here I'm not doing anything fancy, but it's worth pointing out that, in the iSeeThePostOnTheFrontend
method, I'm using the post_id
stored in the $data
array. That array is initialized in the AcceptanceTester
class and is meant exactly to serve as a data sharing buffer between steps that are, in this case, implemented in different traits.
The Editor trait
This trait is not different, in concept, from the Frontend one but proved to be difficult due to the many changes, in UI and interaction, the new WordPress editor brought with it.
The main difference, and the code fully highlights it, is that almost any interaction with the new editor has to happen via its Javascript API.
The WPWebDriver
module inherits, from the base Codeception WebDriver
module, the almighty executeJs
method.
This method executes a string of Javascript code in the page the driven browser is visiting (Chrome in this case, driven by Chromedriver, see the first article for the setup); this is equivalent to opening the developer tools, switching to the console, and executing Javascript code in it:
The trait methods are leveraging this to interact with the editor, add blocks to it and save the post.
Currently, wp-browser does not offer a module dedicated to the new WordPress editor, but I feel like it will soon.
After many trials and errors here's the code of the Test\Editor
trait:
<?php
/**
* Implements all the steps dealing with the backend editor UI; e.g., adding and removing blocks, entering content and
* so on.
*
* @package Test\Gherkin
*/
namespace Test\Traits\Gherkin;
/**
* Trait Editor
*
* @package Test\Gherkin
*/
trait Editor {
/**
* Whether the scenario already logged in or not.
*
* @var bool
*/
protected $logged = false;
/**
* Adds an editor block of the specified type to the current editor.
*
* @param string $type The block name in the format `namespace/name`.
* @param array $props An array of properties to initialize the block with.
* @param null $position The position, in the editor, the block should be inserted at.
* Default to append as last.
*
* @return string The block `clientId`.
*/
protected function addBlock( $type, $props = [], $position = null ) {
$positionString = null !== $position ? ' at position ' . (int) $position : '';
$jsonProps = json_encode( $props, JSON_PRETTY_PRINT );
codecept_debug( "Editor: adding block of type '{$type}'{$positionString} with props:\n" . $jsonProps );
$position = null === $position ? 'null' : (int) $position;
$js = "return (function(){
var block = wp.blocks.createBlock('{$type}', $jsonProps);
wp.data.dispatch('core/editor').insertBlock(block, {$position});
return block.clientId;
})();";
$clientId = $this->executeJS( $js );
codecept_debug( "Editor: added block of type '{$type}'{$positionString}, block clientId is '{$clientId}'." );
return $clientId;
}
/**
* Triggers the save post action on the editor.
*
* After triggering the save action the method will wait for a set number of seconds before
* returning.
*
* @param int $wait How much to wait for the post to save after triggering the save actions.
* The saving will happen asynchronously.
*
* @return int The saved post ID.
*/
protected function savePost( $wait = 2 ) {
codecept_debug( 'Editor: saving the post.' );
$postId = $this->executeJS( 'return (function(){
wp.data.dispatch("core/editor").savePost();
return wp.data.select("core/editor").getCurrentPostId();
})(); ' );
codecept_debug( "Editor: saved the post, post ID is {$postId}" );
$this->wait( $wait );
return $postId;
}
/**
* Clicks the tooltip close ("X") icon to disable tooltips for the current user.
*/
public function disableEditorTips() {
codecept_debug( 'Editor: disabling tips.' );
$this->executeJS( 'document.querySelector("#editor button.nux-dot-tip__disable").click()' );
}
/**
* Edits the post adding properties to it.
*
* The post is not saved after it, use the `savePost` method to save the changes.
*
* @param array $props An associative array of properties to set for the post.
*
* @return int The edited post ID.
*/
protected function editPost( array $props = [] ) {
$jsonProps = json_encode( $props, JSON_PRETTY_PRINT );
codecept_debug( 'Editor: editing the post with properties: ' . $jsonProps );
$postId = $this->executeJS( 'return (function(){
wp.data.dispatch("core/editor").editPost(' . $jsonProps . ');
return wp.data.select("core/editor").getCurrentPostId();
})(); ' );
codecept_debug( "Editor: edited the post, post ID is {$postId}" );
return $postId;
}
/**
* @Given a post of :wordCount words
*
* @param int $wordCount The number of words in the post content.
*/
public function aPostOfWords( $wordCount ) {
if ( ! $this->logged ) {
$this->loginAsAdmin();
}
$this->amOnAdminPage( 'post-new.php' );
$this->disableEditorTips();
$this->fillField( '#post-title-0', 'Test post' );
// Create the test content: the word "test" repeated n times.
$content = implode( ' ', array_fill( 0, (int) $wordCount, 'test' ) );
$this->addBlock( 'core/paragraph', [ 'content' => $content ] );
// Save the post to commit the changes.
$this->editPost( [ 'status' => 'publish' ] );
// Save the post ID in the shared data array.
$this->data['postId'] = $this->savePost();
}
/**
* @Given the :type block is placed at the start of the post
*
* @param string $type The block type, in the format `namespace/type`.
*/
public function theBlockIsPlacedAtTheStartOfThePost( $type ) {
$this->addBlock( $type, [], 0 );
// Save the post to commit the changes.
$this->savePost();
}
}
This code presents an efficiency, and possible cause of errors, in the savePost
method: the arbitrary wait could either be too long or too short leading, in the first case, to wasted running time and, in the second, to false negatives (a test that fails for the wrong reason).
I plan to revisit the code with more time to wait for exactly as much as needed after firing the post save operation; for the time being the method gets the job done and I can run the test to see it fail:
As expected the frontend test is failing as my block is not printing anything of value:
In my next post, I will work to make this test pass and document my progress.
The code for this post can be found on GitHub.