Testing the REST calculator
April 12, 2017
The plugin to test
While I'd usually write the tests before writing any code, in my previous post I've made an exception to have some illustrative code to use.
The plugin provides a single functionality: to resolve simple addition operations through the REST API.
As an example, hitting the /wp-json/calc/add/2/3
would return a 5
. I'm pasting here the plugin code from the previous article to provide some context:
<?php
/*
Plugin Name: REST Calculator
Description: Add numbers using WP REST API!
Version: 0.1.0
Author: Luca Tumedei
Author URI: http://theaveragedev.local
*/
add_action( 'rest_api_init', function () {register_rest_route( 'calc', 'add/(?P<o1>\d+)/(?P<o2>\d+)', [
'methods' => 'GET',
'callback' => [ new Calculator, 'process' ]
] );
} );
class Calculator {
function process( WP_REST_Request $req ) {
try {
$operand_1 = new Operand( $req->get_param( 'o1' ) );
$operand_2 = new Operand( $req->get_param( 'o2' ) );
} catch ( InvalidArgumentException $e ) {
$response = new WP_REST_Response( 'Bad operands' );
$response->set_status( 400 );
return $response;
}
$operation = new Addition( $operand_1, $operand_2 );
$value = $operation->get_value();
set_transient( 'last_operation', $operation . ' with result ' . $value );
$response = new WP_REST_Response( $value );
$response->set_status( 200 );
return $response;
}
}
class Operand {
private $value;
function __construct( $value ) {
if ( ! filter_var( $value, FILTER_VALIDATE_INT ) ) {
throw new InvalidArgumentException( 'Not an int' );
}
$this->value = (int) $value;
}
function get_value() {
return $this->value;
}
}
class Addition {
private $o1;
private $o2;
function __construct( Operand $o1, Operand $o2 ) {
$this->o1 = $o1;
$this->o2 = $o2;
}
function __toString() {
$v1 = $this->o1->get_value();
$v2 = $this->o2->get_value();
$line_1 = 'Add: ' . $v1 . '+' . $v2 . '=' . $this->get_value();
$line_2 = trailingslashit( "Route: /wp-json/calc/add/{$v1}/{$v2}" );
return $line_1 . "\n" . $line_2;
}
function get_value() {
return $this->o1->get_value() + $this->o2->get_value();
}
}
Setting up wp-browser
While I've detailed the steps to get up and running with Codeception and wp-browser multiple times in previous posts, the current, fastest way to be up and running with it starting from scratch is to use the interactive bootstrap command; in the plugin or theme root folder type:
composer init
composer require --dev lucatume/wp-browser
.vendor/bin/wpcept bootstrap --interactive
Time spent to read notes and questions shown and asked by the command output is time well spent.
As a bonus add vendor/bin
to your path to avoid having to type ./vendor/bin/
before each codecept
or wpcept
call.
The acceptance umbrella
I've concluded the previous post promising to write at least a test of each type for the plugin, and the first test I'm writing will be, as I would normally do, an acceptance one.
After the usual wp-browser setup drill I scaffold the first test in the acceptance
suite:
wpcept generate:cest acceptance AddRequest
Since the plugin does not expose any UI and its whole request handling ends in a REST API response, I'm adding Codeception own REST module to the acceptance
suite:
# Codeception Test Suite Configuration
# Suite for WordPress acceptance tests.
# Perform tests using or simulating a browser.
class_name: AcceptanceTester
modules:
enabled:
- \Helper\Acceptance
- WPDb
- WPBrowser
- REST
config:
WPDb:
dsn: 'mysql:host=localhost;dbname=wp'
user: root
password: root
dump: tests/_data/dump.sql
populate: true
cleanup: true
url: 'http://wp.dev'
tablePrefix: wp_
WPBrowser:
url: 'http://wp.dev'
adminUsername: admin
adminPassword: admin
adminPath: /wp-admin
REST:
depends: WPBrowser
url: 'http://wp.dev/wp-json'
and rebuild the suite:
codecept build
The first test case I'm writing is to make sure that 4 + 5
yields 9
in the tests/acceptance/AddRequestCest.php
file:
<?php
class AddRequestCest {
/**
* It should return the correct result
* @test
*/
public function return_the_correct_result( AcceptanceTester $I ) {
$I->sendGET( '/calc/add/4/5' );
$I->seeResponseCodeIs( 200 );
$I->seeResponseEquals( 9 );
}
}
Now to add some flexibility to the test method and tap into something similar to PHPUnit data providers I'm using Codeception example system for acceptance testing to keep the code DRY and reuse the logic:
<?php
class AddRequestCest {
/**
* It should return the correct result
* @test
*
*
* @example [5, 4, 9]
* @example [5, 0, 5]
* @example [5, -5, 0]
* @example [5, -8, -3]
* @example [0, 0, 0]
* @example [-1, -3, 4]
*/
public function return_the_correct_result( AcceptanceTester $I , \Codeception\Example $example ) {
$first = $example[0];
$second = $example[1];
$expected = $example[2];
$I->sendGET( "/calc/add/{$first}/{$second}" );
$I->seeResponseEquals( $expected );
}
}
And I immediately run into failures.
[](http://theaveragedev.local/wp-content/uploads/2017/04/acceptance-failure-1.png)
Any test that is using non integer positive numbers or zero value is failing.
Time to move down a level and write some functional testing.
The functional "poking at stuff" hypothesis
Refering to the previous post flowchart the purpose of functional testing is to assert that the code implementation (the "how") works as intended, opposed to the acceptance testing approach of asserting the expected behaviour from a consumer perspective (the "what").
The simplicity of the plugin shows in how similar the functional test is to an acceptance test once I add it to the suite:
wpcept generate:cest functional AddRequest
and fill in the test case:
<?php
class AddRequestCest {
/**
* It should store last operation result
* @test
*
* @example [5,4]
* @example [5,0]
* @example [5,-5]
* @example [5,-8]
* @example [0,0]
* @example [-1,-3]
*/
public function return_the_correct_result( FunctionalTester $I, \Codeception\Example $example ) {
$first = $example[0];
$second = $example[1];
$I->amOnPage( "/wp-json/calc/add/{$first}/{$second}" );
$I->seeResponseCodeIs( 200 );
$I->seeOptionInDatabase( [ 'option_name' => '_transient_last_operation' ] );
}
}
Again a streak of errors:
[](http://theaveragedev.local/wp-content/uploads/2017/04/functional-failure-1.png)
Integration testing will help digging deeper.
The integration unwilling inspection
Integration testing is meant to test application "modules" and in this application case there is only one: the one handling the addition operation request.
The module as a whole hooks into WordPress internals and in its REST API infrastructure, and its entry point is the Calculator
class itself.
I add a test case to the integration
suite:
wpcept generate:wpunit integration AddEndpointHandler
Since the code is so simple there is no surprise in the fact that the integration test looks very similar to the other tests:
<?php
class AddEndpointHandlerTest extends \Codeception\TestCase\WPTestCase {
public function inputsAndOutputs() {
return [
[ 5, 4, 9 ],
[ 5, 0, 5 ],
[ 5, - 5, 0 ],
[ 5, - 8, - 3 ],
[ 0, 0, 0 ],
[ - 1, - 3, 4 ]
];
}
/**
* It should return the correct result
* @test
*
* @dataProvider inputsAndOutputs
*/
public function return_the_correct_result( $o1, $o2, $expected ) {
$request = new WP_REST_Request();
$request->set_param( 'o1', $o1 );
$request->set_param( 'o2', $o2 );
$module = new Calculator();
$result = $module->process( $request );
$this->assertInstanceOf( WP_REST_Response::class, $result );
$this->assertEquals( $expected, $result->data );
}
}
And the resemblance extends to the test results too:
[](http://theaveragedev.local/wp-content/uploads/2017/04/integration-failure-1.png)
The unit intrusion
Down to the inner level of testing this is probably where the problem will be found and, hopefully, solved.
Scaffolding an unit tests for the Operand
class is, again, done using Codeception CLI utility:
wpcept generate:test unit Operand
Filling in the tests gives the responsible code away:
<?php
namespace Tests\Unit;
use Operand;
class OperandTest extends \Codeception\Test\Unit {
/**
* @var \UnitTester
*/
protected $tester;
public function notAnInt() {
return [
[ 'foo' ],
[ (object) [ 'foo' => 'bar' ] ],
[ 234234.234 ],
[ 1.3 ]
];
}
/**
* It should throw if value is not an int
* @test
*
* @dataProvider notAnInt
*/
public function throw_if_value_is_not_an_int() {
$this->expectException( \InvalidArgumentException::class );
new Operand( 'foo' );
}
/**
* It should support positive integer values
* @test
*/
public function support_positive_integer_values() {
new Operand( 23 );
}
/**
* It should support negative integer values
* @test
*/
public function support_negative_integer_values() {
new Operand( - 23 );
}
/**
* It should support zero value
* @test
*/
public function support_zero_value() {
new Operand( 0 );
}
}
After some code fixing all the tests will pass:
[](http://theaveragedev.local/wp-content/uploads/2017/04/tests-pass.png)
The mandatory "use your judgment" conclusion
The plugin code, tests included, is on GitHub.
I've added an underwhelming number of tests just to showcase some ideas; it could provide some tutorial value still so, while the tests in place are a start, they are far from being the end of it.
While I've shown my take on what kind of test should be in charge of testing what component and at what level, in the end each tester will use different techniques and methods and concentrate on different parts: experience and technique will vary but the importance of testing won't.