Testing the REST calculator

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.com
*/

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.

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:

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:

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:

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.

I appreciate your input