Setting up a plugin to run some functional testing.
Tongues
To test the new functional tests dedicated wp-browser module, WordPress
, I will develop yet another multi-language plugin.
This is a toy project intended to experiment and I will probably drop it once all the tests I’m interested in will be in.
On GitHub
This code I present here is tagged 0.0.1
on GitHub.
Setting up Composer
I’ve created a basic Composer configuration file in the plugin root folder and required all the production and development dependencies I will be using:
{
"name": "lucatume/tongues",
"description": "Yet another WordPress language plugin",
"minimum-stability": "stable",
"license": "GPL-2.0",
"authors": [
{
"name": "Luca Tumedei",
"email": "luca@theaveragedev.com"
}
],
"require-dev": {
"lucatume/wp-browser": "^1.15"
},
"require": {
"php": ">=5.4",
"lucatume/di52": "^1.3"
},
"autoload": {
"psr-4": {
"Tongues\\": "src/"
}
}
}
I will require PHP version 5.4
, wp-browser and Codeception for the tests and the di52 dependency injection container to handle dependency injection and code organization.
I’ve set up the project initializing Composer and wp-browser from the plugin root folder:
composer update
./vendor/bin/wpcetp bootstrap
Functional suite configuration
The bootstrap command did create suites configuration files for me; the acceptance
, integration
and unit
suites are the usual ones but the functional.suite.yml
file is worth some additional consideration:
# Codeception Test Suite Configuration
# Suite for WordPress functional tests.
# Emulate web requests and make the WordPress application process them.
class_name: FunctionalTester
modules:
enabled:
- \Helper\Functional
- Filesystem
- WPDb:
dsn: 'mysql:host=127.0.0.1;dbname=lang'
user: root
password: root
dump: tests/_data/dump.sql
populate: true
cleanup: true
url: 'http://lang.dev'
tablePrefix: wp_
- WordPress:
depends: WPDb
wpRootFolder: /Users/Luca/Sites/lang
adminUsername: admin
adminPassword: admin
The functional suite depends on the WPDb
module to work and, beside that, will have to be pointed to the WordPress installation root folder, the one containing the plugin I’m testing, and provided the admin user login information.
Boilerplate setup
After the suites configuration files are set up it’s time to provide the WPDb
module the database dump that will constitute the database initial fixture for each acceptance and functional test.
I’ve set up the multisite network to contain 3 blogs:
- the default one at
http://lang.dev
with ablog_id
of1
- the Italian one at
http://it.lang.dev
with ablog_id
of2
- the French one at
http://fr.lang.dev
with ablog_id
of3
and activated the currently empty “Tongues” plugin. This will be my functional and acceptance database testing fixture and I will use WP-CLI to dump the database in the Tongues plugin tests/_data
folder:
wp db export ./Users/Luca/Sites/lang/wp-content/plugins/tongues/tests/_data/dump.sql
Before I write any code I will run Codeception to make sure all the modules are working as intended:
codecept run
The plugin initial code is the bare-bones code needed to make it register as a plugin in WordPress and be able to activate it:
<?php
/**
* Plugin Name: Tongues
* Plugin URI: http://theAverageDev.com
* Description: Yet another WordPress language plugin
* Version: 1.0
* Author: theAverageDev
* Author URI: http://theAverageDev.com
* License: GPL 2.0
*/
include 'vendor/autoload.php';
The last line will take care to include the autoload file Composer generated for me.
Preliminary decisions
As the description states the plugin is another variation of the multi-language problem solution and it will rely on multi-site installations to provide each language from a different blog.
As an example the English version will be served by the blog at lang.dev
, the Italian version from the blog at it.lang.dev
and the French version from the blog at fr.lang.dev
.
But how to tell the plugin that one blog is for the English version, one is for the Italian one and so on?
WordPress will, create, when setting up a multi-site installation, the blogs
table containing the information about the blogs on the network; the last column in the table is aptly called lang_id
:
The codex entry for the get_blog_details
function states:
lang_id - (integer) ID of the language this blog is written in.
I will leverage that column to keep track of the various blog languages; that column will store an integer value that will map to language code.
Language codes cannot be hardcoded though and the super-admin should be able to specify which blog in the network will serve which language; e.g. the blog with a blog_id
of 1
will have a lang_id
that will map to en
, the blog with blog_id
2
will have a lang_id
mapping to it
and so on.
To allow the super-admin to set those information I’ve set up a basic network options page and the only markup I’m printing on the page is a nonce field.
A dedicated service provider (it comes from the DI52 package) will hook the page:
<?php
namespace Tongues\ServiceProviders;
class NetworkAdminPage extends \tad_DI52_ServiceProvider {
/**
* Binds and sets up implementations.
*/
public function register() {
$this->container->bind( 'Tongues\\UI\\Admin\\NetworkOptionsInterface', 'Tongues\\UI\\Admin\\NetworkOptions' );
$container = $this->container;
add_action( 'network_admin_menu', function () use ( $container ) {
$title = __( 'Tongues', 'tongues' );
add_submenu_page(
'settings.php',
$title,
$title,
'manage_network',
'tongues-network-options',
[ $container->make( 'Tongues\\UI\\Admin\\NetworkOptionsInterface'), 'render' ]
);
} );
}
/**
* Binds and sets up implementations at boot time.
*/
public function boot() {
// TODO: Implement boot() method.
}
}
and the page class itself will deal with the concise printing.
<?php
namespace Tongues\UI\Admin;
class NetworkOptionsInterfaceInterface
extends AbstractOptionsPageInterface
implements OptionsPageInterface, NetworkOptionsPageInterface {
protected $nonceAction = 'tongues-network-options';
public function render() {
wp_nonce_field( $this->getNonceAction(), $this->getNonceField() );
}
}
The functional and acceptance tests difference
The page in its current state is, as one might guess, empty.
It contains but a fundamental piece of code: the nonce field. This will allow me to send authenticated HTTP requests to the backend and a functional test does not need any UI to be in place to be written.
Forms and their components are merely a way to assemble the structured data needed to make a meaningful request.
Was I to write an acceptance test I would have needed an actual UI to interact with but a functional test will not need that.
Right now, provided the receiving end was in place, I could write a test like this:
<?php
namespace REST;
use FunctionalTester;
class NetworkOptionsCest {
/**
* @test
* it should mark bad request if trying to update lang_id for non existing blog_id
*/
public function it_should_mark_bad_request_if_trying_to_update_lang_id_for_non_existing_blog_id( FunctionalTester $I ) {
$I->loginAsAdmin();
$I->amOnAdminPage( '/network/settings.php?page=tongues-network-options' );
$I->haveHttpHeader( 'X-WP-Nonce', $I->grabValueFrom( 'input[name="tongues-nonce"]' ) );
$I->sendAjaxPostRequest( '/wp-json/tongues/v1/network-config', [
'lang_ids' => [ 'blog_id' => 21, 'lang_id' => 23 ]
] );
$I->canSeeResponseCodeIs( '400' );
}
}
Next
I will implement the REST API endpoints and code needed to handle the first requests and have the first functional tests passing.