Developing Codeception templates for fun and profit - 1

A use case and the bare minimum code.

A new possibility

Version 2.3 of Codeception introduced templates.
Templates are a way to scaffold project files, fundamental Codeception files like configuration files and accessory files alike, in a way that “smells like a script” but that is integrated and shipped with a Codeception library.
wp-browser itself uses a template to scaffold WordPress specific configurations it requires.
Templates are not limited, in their use, to large projects only and can instead be tapped into to ship a cross-compatible scripting system.
Developing a template is easy and fun (if you are into this sort of things), and doing so with a test-driven development approach is not that difficult.
I will start with a basic example first.

An example usage

Forget about complex folder structures and nested files: this example project objective is to allow a team of developers to quickly scaffold JSON testing files for an application the team is developing; these JSON files will be used in the tests and shared with the team.
The team is developing a PHP application that relies on a JSON response provided by an external service, the service cannot be hit or assumed available during the tests and hence any one of its responses has to be mocked.
The JSON file will feed test methods like this one:

// a PHPUnit-like test case
public function test_user_is_marked_blocked_if_blocked_by_remote_service(){
        // prepare the mock dependency
    $httpHandler = $this->prophesize(HttpHandler::class);
    $blockedResponseFile = codecept_data_dir('responses/user-blocked.json')
    $httpHandler->getUserStatus(23)->willReturn(file_get_contents($blockedResponseFile));
    $activeResponseFile = codecept_data_dir('responses/user-active.json')
    $httpHandler->getUserStatus(89)->willReturn(file_get_contents($activeResponseFile));

    // use a Phrophecy mock
    $serviceClient = new ServiceClient($httpHandler->reveal());

    $this->assertFalse($serviceClient->isUserActive(23)));
    $this->assertTrue($serviceClient->isUserActive(89));
}

The remote service that is being mocked in the tests provides responses in a number of formats all falling into this scheme:

{
    "code": <code>,
    "key": <key>,
    "message": <message>
}

Recently the service version has been updated to 2 and the response format has been enriched:

{
    "version": "2",
    "code": <code>,
    "key": <key>,
    "message": <message>
}

In version 1 of the remote service the key value is the md5($sharedSecret . $message); in version 2 the key value is instead md5($sharedSecret . $message . $code).
The getaway from all this introduction is: creating a mock response that will pass the checks for format and value is a manual, time-consuming and error-prone process.
Knowing of the new template possibility a smart developer could provide its team with a toll like this:

codecept init serviceresponse

And build a mock response provided some data.

The bare minimum code

To provide a basic template that would allow developers on the team to scaffold mock responses I will take the shortest route and put in place the code that should fulfill the following points:

  1. ask the mock response version (1 or 2)
  2. ask the user to choose the mock response code
  3. ask the user to choose message body

For the time being the process will involve some knowledge of the message codes and error messages on the developers side but this kind of comes with the job.
To have the template become available on the codecept init the first step is to create a class in the Codeception\Template namespace; following a classic scheme I assume the template code will live in the src/Codeception/Template/ServiceResponse.php file.
The namespace will make the file be detected by Codeception as a template; the filename, ServiceResponse, will dictate its template name: servicename.
In its most basic form the src/Codeception/Template/ServiceResponse.php file looks like this:

<?php

namespace Codeception\Template;

use Codeception\InitTemplate;

class ServiceResponse extends InitTemplate {

    /**
     * Override this class to create customized setup.
     *
     * @return mixed
     */
    public function setup() {
        echo "Hello world!";
    }
}

This is all is required to be able to run the init command and see the result:

Putting some code into place

The code required to put together a template that does what is required from the template is not that much complicated and build on the power provided by Codeception and its dependencies (notable among those Symfony components):

<?php

namespace Codeception\Template;

use Codeception\InitTemplate;

class ServiceResponse extends InitTemplate {

    /**
     * Override this class to create customized setup.
     *
     * @return mixed
     */
    public function setup() {
        $version = null;
        while (!in_array($version, ['1', '2'])) {
            $version = $this->ask('Service version?', '2');
        }

        $code = null;
        while (!in_array($code, ['user-blocked', 'user-active', 'success', 'error', 'not-authorized', 'refused'])) {
            $code = $this->ask('Service message code?', 'success');
        }

        $message = $this->ask('Service message content?', 'A service message.');

        $sharedSecret = $this->ask('Shared secret?', 'secret');

        $key = $version == '1'
            ? md5($sharedSecret . $message)
            : md5($sharedSecret . $message . $code);

        $response = [
            'key'     => $key,
            'code'    => $code,
            'message' => $message,
        ];

        if ($version == '2') {
            $response['version'] = '2';
        }

        $name = null;
        while (empty($name)) {
            $name = $this->ask('Service mock response file name?', 'some-response');
        }

        $json = json_encode($response);

        if (!is_dir(codecept_data_dir('responses'))) {
            if (!mkdir(codecept_data_dir('responses')) && !is_dir(codecept_data_dir('responses'))) {
                throw new \RuntimeException('Could not create the responses directory');
            }
        }

        file_put_contents(codecept_data_dir("responses/{$name}.json"), $json);

        $this->saySuccess("Mock server response {$name}.json created.");
    }
}

The script can, then, be put into motion the same way as before:

Next

While putting together the current version of the template did not require more than a little time, building for scale and trying to approach the development of the template with a test-driven development approach will require a little more time and insight.