Developing a plugin using DI and TDD 15

Showing the votes.

What’s this all about?

This post moves on the step by step development of a simple votes plugin using dependency injection and test-driven development.
The posts alone might not be as interesting without a peek at the plugin code and probably a skim over the first post in the series and the following ones.
Tools of the trade are DI52 to handle PHP 5.2 compatible dependency injection, Codeception and wp-browser to handle the testing.

What will it need?

The meta box responsible for showing the votes will be implemented by the idlikethis_MetaBoxes_VotesDisplayMetaBox class. As I’ve already done when implementing the class responsible for the rendering of the shortcode I will not test the output itself as much as how the class interacts and communicates with its “surroundings”; in development terms the class surroundings are its dependencies.
The meta box will use a template rendering engine to render a simple report about the votes; since the votes are comment based another class dependency will be the comments repository and, sticking to a model again already adopted for the shortcode rendering class, a text provider.
This latter dependency in particular requires some explanation: the idlikethis_Texts_VotesMetaboxTextProviderInterface interface prototypes a class with the only responsibility to provide the meta box rendering class with default texts; this might prove useful in future iterations decoupling the meta box rendering class from the texts it will use as part of the output and vice versa. Should a proto meta box be displayed in another place that’s not the back-end (e.g. the front-end) then the same texts might apply and duplicating those in two places will prove a violation of the DRY principle.
So here’s the new idlikethis_MetaBoxes_VotesDisplayMetaBox class __construct method signature:

    public function __construct(idlikethis_Repositories_CommentsRepositoryInterface $comments_repository, idlikethis_Templates_RenderEngineInterface $render_engine, idlikethis_Texts_VotesMetaboxTextProviderInterface $texts);

Testing the meta box class

The Codeception suite makes a short work of the new test scaffolding

wpcept generate:wpunit wpunit "idlikethis\MetaBoxes\VotesDisplayMetaBox"

and the usual test drill will lead to the test code below.
Not testing the output shortens the test code considerably reducing it to pretty much a cover of all the paths the logic could take: in this case either there are votes or not.

namespace idlikethis\MetaBoxes;

use idlikethis_MetaBoxes_VotesDisplayMetaBox as VotesDisplayMetaBox;
use Prophecy\Argument;

class VotesDisplayMetaBoxTest extends \Codeception\TestCase\WPTestCase
{

    /**
     * @var \idlikethis_Repositories_CommentsRepositoryInterface
     */
    protected $commments_repository;

    /**
     * @var \idlikethis_Texts_VotesMetaboxTextProviderInterface
     */
    protected $texts_provider;

    /**
     * @var \idlikethis_Templates_RenderEngineInterface
     */
    protected $render_engine;

    public function setUp()
    {
        // before
        parent::setUp();

        // your set up methods here
        $this->commments_repository = $this->prophesize("idlikethis_Repositories_CommentsRepositoryInterface");
        $this->render_engine = $this->prophesize('idlikethis_Templates_RenderEngineInterface');
        $this->texts_provider = $this->prophesize("idlikethis_Texts_VotesMetaboxTextProviderInterface");
    }

    public function tearDown()
    {
        // your tear down methods here

        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable()
    {
        $sut = $this->make_instance();

        $this->assertInstanceOf('idlikethis_MetaBoxes_VotesDisplayMetaBox', $sut);
    }

    /**
     * @test
     * it should render an empty message if there are no idlikethis comments associated to the post
     */
    public function it_should_render_an_empty_message_if_there_are_no_idlikethis_comments_associated_to_the_post()
    {
        $post = $this->factory()->post->create_and_get();

        $this->commments_repository->get_comments_for_post($post)->willReturn([]);
        $this->texts_provider->get_empty_comments_text()->shouldBeCalled();
        $this->render_engine->render(Argument::any(), Argument::any())->shouldBeCalled();

        $sut = $this->make_instance();
        $sut->render($post, []);
    }

    /**
     * @test
     * it should render the comments count display if there are idlikethis comments associated with the post
     */
    public function it_should_render_the_comments_count_display_if_there_are_idlikethis_comments_associated_with_the_post()
    {
        $post = $this->factory()->post->create_and_get();
        $comments = $this->factory()->comment->create_many(10);

        $this->commments_repository->get_comments_for_post($post)->willReturn(['first idea' => array_splice($comments, 0, 5), 'second idea' => $comments]);
        $this->texts_provider->get_comments_title_text()->shouldBeCalled();
        $this->render_engine->render(Argument::any(), Argument::any())->shouldBeCalled();

        $sut = $this->make_instance();
        $sut->render($post, []);
    }

    private function make_instance()
    {
        return new VotesDisplayMetaBox($this->commments_repository->reveal(), $this->render_engine->reveal(), $this->texts_provider->reveal());
    }

}

The resulting class code is the one below:

class idlikethis_MetaBoxes_VotesDisplayMetaBox implements idlikethis_MetaBoxes_VotesDisplayMetaBoxInterface
{
    /**
     * @var idlikethis_Repositories_CommentsRepositoryInterface
     */
    protected $comments_repository;

    /**
     * @var idlikethis_Texts_VotesMetaboxTextProviderInterface
     */
    protected $texts;

    /**
     * @var idlikethis_Templates_RenderEngineInterface
     */
    protected $render_engine;

    /**
     * @var string
     */
    protected $template_slug;

    /**
     * @var array
     */
    protected $template_data;

    public function __construct(idlikethis_Repositories_CommentsRepositoryInterface $comments_repository, idlikethis_Templates_RenderEngineInterface $render_engine, idlikethis_Texts_VotesMetaboxTextProviderInterface $texts)
    {
        $this->comments_repository = $comments_repository;
        $this->render_engine = $render_engine;
        $this->texts = $texts;

        $this->template_slug = 'metaboxes/votes';
        $this->template_data = array();
    }

    /**
     * Returns the meta box id.
     *
     * @return string
     */
    public function id()
    {
        return 'idlikethis-vote-display';
    }

    /**
     * Returns the meta box title.
     *
     * @return string
     */
    public function title()
    {
        return __("I'd like to see the votes", 'idlikethis');
    }

    /**
     * Returns the screen(s) the meta box should display on.
     *
     * @return string|array|WP_Screen
     */
    public function screen()
    {
        return array('post', 'page');
    }

    /**
     * Returns the context the meta box should display into.
     *
     * @return string
     */
    public function context()
    {
        return 'side';
    }

    /**
     * Returns the priority for the meta box.
     *
     * @return string
     */
    public function priority()
    {
        return 'high';
    }

    /**
     * Echoes the meta box markup to the page.
     *
     * @param string|WP_Post|array $object
     * @param array|mixed $box The meta box registration description.
     * @return void
     */
    public function render($object, $box)
    {
        $comments = $this->comments_repository->get_comments_for_post($object);
        if (empty($comments)) {
            $this->template_data['has_comments'] = false;
            $this->template_data['header_text'] = $this->texts->get_empty_comments_text();
        } else {
            $this->template_data['has_comments'] = true;
            $this->template_data['header_text'] = $this->texts->get_comments_title_text();
            array_walk($comments, array($this, 'count_comments'));
            arsort($comments);
            $this->template_data['rows'] = $comments;
        }

        echo $this->render_engine->render($this->template_slug, $this->template_data);
    }

    protected function count_comments(array &$comments, $text)
    {
        $comments = count($comments);
    }
}

Service providing

To cope with the new needs of the meta box class I’ve modified the MetaBoxes service provider along with it to bind and provide any needed dependency

class idlikethis_ServiceProviders_MetaBoxes extends tad_DI52_ServiceProvider
{

    /**
     * Binds and sets up implementations.
     */
    public function register()
    {
        add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
    }

    public function add_meta_boxes()
    {
        $this->container->singleton('idlikethis_Texts_VotesMetaboxTextProviderInterface', 'idlikethis_Texts_VotesMetaboxTextProvider');

        $this->container->bind('idlikethis_MetaBoxes_VotesDisplayMetaBoxInterface', 'idlikethis_MetaBoxes_VotesDisplayMetaBox');
        $this->container->bind('idlikethis_MetaBoxes_PostControlMetaBoxInterface', 'idlikethis_MetaBoxes_PostControlMetaBox');

        $this->container->tag(array(
            'idlikethis_MetaBoxes_VotesDisplayMetaBoxInterface',
            'idlikethis_MetaBoxes_PostControlMetaBoxInterface',
        ), 'meta-boxes');

        /** @var idlikethis_MetaBoxes_MetaBoxInterface $meta_box */
        foreach ($this->container->tagged('meta-boxes') as $meta_box) {
            add_meta_box($meta_box->id(), $meta_box->title(), array($meta_box, 'render'), $meta_box->screen(), $meta_box->context(), $meta_box->priority());
        }
    }

}

The two meta boxes output has changed accordingly in the back-end

Votes and control in the BE

The other classes I’ve extended, with tests where applicable, accordingly.

Adjustments and code

I would appreciate someone furiously clicking the votes button to express an opinion but I’ve reworked some of the JavaScript code to leverage local storage and limit that to a one hour interval.
The code current to this post is on GitHub.

Next

Moving to the votes control meta box to begin wrapping the project.