Developing Codeception templates for fun and profit - 2

Testing a Codeception template.

A simple use case

In my previous article I’ve shown a simple use case of a Codeception template used to make life easier for a team of developers collaborating on a project.
The template, in this fictional example, will guide developers through the creation of a source file to test code depending on an API provided by a remote service.
In the end the class proved simple enough to code:

namespace Codeception\Template;

use Codeception\InitTemplate;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

class ServiceResponse extends InitTemplate {

    /**
     * @var QuestionHelper
     */
    protected $questionHelper;

    public function __construct(InputInterface $input, OutputInterface $output, HelperInterface $questionHelper = null) {
        parent::__construct($input, $output);
        $this->questionHelper = $questionHelper === null ? new QuestionHelper() : $questionHelper;
    }

    /**
     * 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 = $this->key($version, $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.");
    }

    protected function ask($question, $answer = null) {
        $question = "? $question";
        $dialog = $this->questionHelper;
        if (is_array($answer)) {
            $question .= " <info>(" . $answer[0] . ")</info> ";
            return $dialog->ask($this->input, $this->output, new ChoiceQuestion($question, $answer, 0));
        }
        if (is_bool($answer)) {
            $question .= " (y/n) ";
            return $dialog->ask($this->input, $this->output, new ConfirmationQuestion($question, $answer));
        }
        if ($answer) {
            $question .= " <info>($answer)</info>";
        }
        return $dialog->ask($this->input, $this->output, new Question("$question ", $answer));
    }

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

        return $key;
    }
}

In this post I will work on this code to test it and make it sure it behaves as intended.

How to test a Codeception template?

While it’s easy enough to define what tests are needed for a standard PHP application the same task might prove less straightforward when it comes at testing a testing tool like this one.
Since I can rely on Codeception code to make its job what I really need to test is this class only and a unit test will be more than enough to cover it.
From here on I will show test methods part of the test case generated with this command:

codecept generate:test unit "Codeception\Template\ServiceResponse"

Here is the class in its initial form:

namespace Codeception\Template;


class ServiceResponseTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    protected function _before()
    {
    }

    protected function _after()
    {
    }

    // tests
    public function testSomeFeature()
    {

    }
}

Regaining control of the dependencies

Since I know the code, in its current form, is already working the first test I want to write is one asserting the existing state: it does what it is supposed to do when provided all the correct inputs.
And here it comes the first challenge: how to “mock” the user interactions? The template will use and require user input to work so that is going to be something I must mock and control in a test. Can it be done?
Not out of the box; the template system, based on Symfony Console components, uses an hard-coded QuestionHelper dependency to ask questions and retrieve answers from the user, this is the Codeception\Template\InitTemplate::ask method implementation:

    protected function ask($question, $answer = null)
    {
        $question = "? $question";
        $dialog = new QuestionHelper();

        //...
   }

The QuestionHelper is a dependency I need to mock in the tests to simulate the user answers so I simply override the method in the Codeception\Template\ServiceResponse class and inject a dependency in the class constructor method:

namespace Codeception\Template;

use Codeception\InitTemplate;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

class ServiceResponse extends InitTemplate {

    /**
     * @var QuestionHelper
     */
    protected $questionHelper;

    public function __construct(InputInterface $input, OutputInterface $output, HelperInterface $questionHelper = null) {
        parent::__construct($input, $output);
                // injected dependency    
        $this->questionHelper = $questionHelper === null ? new QuestionHelper() : $questionHelper;
    }

    /**
     * Override this class to create customized setup.
     *
     * @return mixed
     */
    public function setup() {
        // ...
    }

    protected function ask($question, $answer = null) {
        $question = "? $question";
                // not hard-coded anymore
        $dialog = $this->questionHelper;

        // ...
    }
}

I’m allowing a null argument for the questionHelper in the constructor since Codeception will not inject that dependency when building the template class while using codecept init serviceresponse; I will, though, in the tests.
Now this “dependency inject-ability” issue is addressed it is time for the first test.

The first test

Since the first test is asserting the complete and correct functionality of the template, it provides an excellent starting point as it will force me to put in place almost all the “tools” (methods really) I will need to complete a test for the class; after some work here is the test case in its first iteration:

namespace Codeception\Template;


use function foo\func;
use PHPUnit\Runner\Exception;
use Prophecy\Argument;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;

class ServiceResponseTest extends \Codeception\Test\Unit {

    /**
     * @var \UnitTester
     */
    protected $tester;

    /**
     * @var InputInterface
     */
    protected $input;

    /**
     * @var OutputInterface
     */
    protected $output;

    /**
     * @var QuestionHelper
     */
    protected $questionHelper;

    /**
     * @var array
     */
    protected $toDelete = [];

    protected function _before() {
        $this->input          = $this->prophesize(InputInterface::class);
        $this->output         = $this->prophesize(OutputInterface::class);
        $this->questionHelper = $this->prophesize(QuestionHelper::class);

        // some basic input/output redirection and setup
        $this->output->getFormatter()->willReturn(new OutputFormatter());
        $this->output->writeln(Argument::type('string'))->willReturn(null);
    }

    protected function _after() {
        if (!empty($this->toDelete)) {
            foreach ($this->toDelete as $file) {
                if (file_exists($file)) {
                    unlink($file);
                }
            }
        }
    }

    public function test_will_scaffold_mock_service_response() {
        $file             = codecept_data_dir('responses/foo.json');
        $this->toDelete[] = $file;
        $this->mockAnswersWithMap([
            'service version'         => '2',
            'service message code'    => 'user-blocked',
            'service message content' => 'User blocked for reasons',
            'shared secret'           => 'secret',
            'file name'               => 'foo',
        ]);

        $sut = $this->make_instance();

        $sut->setup();

        $this->assertFileExists($file);
        $decoded = json_decode(file_get_contents($file), true);
        $this->assertArrayHasKey('key', $decoded);
        $this->assertArrayHasKey('code', $decoded);
        $this->assertArrayHasKey('message', $decoded);
        $this->assertArrayHasKey('version', $decoded);
        $this->assertEquals('user-blocked', $decoded['code']);
        $this->assertEquals('User blocked for reasons', $decoded['message']);
        $this->assertEquals('2', $decoded['version']);
        $this->assertEquals($sut->key('2', 'secret', 'User blocked for reasons', 'user-blocked'), $decoded['key']);
    }

    /**
     * @return ServiceResponse
     */
    protected function make_instance() {
        return new ServiceResponse($this->input->reveal(), $this->output->reveal(), $this->questionHelper->reveal());
    }

    /**
     * @param $questionAnswersMap
     */
    protected function mockAnswersWithMap($questionAnswersMap) {
        $this->questionHelper->ask(
                    $this->input, 
                    $this->output, 
                    Argument::type(Question::class))->will(function ($args) use ($questionAnswersMap) {
            $question     = $args[2];
            $questionText = $question->getQuestion();

            $answer = false;
            foreach ($questionAnswersMap as $key => $value) {
                if (preg_match('/' . preg_quote($key, '/') . '/i', $questionText)) {
                    $answer = $value;
                    break;
                }
            }

            if ($answer === false) {
                throw new Exception("Question with text [{$questionText}] is not a mocked answer");
            }

            return $answer;
        });
    }
}

And its test status:

Worth noting here is: * how I’m using the injected QuestionHelper instance to mock the answers with a measure of flexibility provided by the use of regular expressions in the mockAnswersWithMap method * how I’m using the _before and _after methods to set up and tear down the mocks (handled by the mighty Prophecy mocking engine) * how I’ve delegated the construction of the subject under test (“sut”) and the injection of its dependencies to the makeInstance method; this proves immensely powerful as test methods stack and test maintain-ability becomes an issue

Pretty much all there is boiler-plate testing code still this is a good example of how some patterns keep proving themselves useful over and over.

Testing for a failure

Now that I know the full process is working, down to the file contents, it’s time to test for a failure; I want now to make sure the user is explicitly told that an empty destination file name is not allowed.
Here is the test method:

public function test_will_not_allow_for_empty_file_name() {
    // in case of failure
    $file             = codecept_data_dir('responses/.json');
    $this->toDelete[] = $file;
    $this->mockAnswersWithMap([
        'service version'         => '2',
        'service message code'    => 'user-blocked',
        'service message content' => 'User blocked for reasons',
        'shared secret'           => 'secret',
        'file name'               => '',
    ]);

    $sut = $this->make_instance();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessageRegExp('/file name/i');

    $sut->setup();

    $this->assertFileNotExists($file);
}

A failure at first:

And a pass after updating the ServiceResponse::setup method to this:

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 = $this->key($version, $sharedSecret, $message, $code);

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

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

    $name = $this->ask('Service mock response file name?', 'some-response');
    if (empty($name)) {
        throw new \InvalidArgumentException('File name must not be empty.');
    }

    $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.");
}

Iterating this way complex, as complex as a PHP application can get, templates can be created.
The mini series of two posts was born out of a project I’ve been asked to collaborate to where the same result, scaffolding some test source files, was obtained with an obscure and non-testable cross-platform scripting approach. Codeception becomes more and more powerful every day: tap into that.