How to use PHPSpec and Magento 2 a quick start

Posted February 6, 2016

Using PHPSpec in Magento 1 was never an easy task. There was no Dependency Injection so it made it hard to actually inject dependencies into our classes. This in turn had the knock on effect of forcing us to actually test the classes our subjects were being used in. Whats the big deal I hear you say? Well. Why should we have to bootstrap the entire framework just to unit test a piece of behavior we are developing? Most of the time ( and if we are designing ) our code well we should be de coupled from the framework so there is a clear divide between business logic and framwework code. more on this in future posts

Now this has all changed, Setting up and using PHPSpec in our code is simples. Lets start by using composer :) Yes composer to install the dependencies into our project.

Open up your composer.json manifest for the project ( if you are using the docker images I am then the sources files are located in src folder.) Hunt down the line that defines require-dev and add in:

"phpspec/phpspec": "2"

Save the file and a quick composer update and we will have bin/phpspec at our disposal.

Now we have our unit testing tool installed lets have a talk about what we are going to build. Well I wanted to show something that is simple and as we are learning PHPSpec and de coupled design here I didn’t want to have to cover lots of Magento 2 concepts. So we are going to add a new command to bin/magento that will say Hello $name; not what you might call rocket science and probably violates all of the design principles for over engineering but meh.

Open up your terminal and run ./bin/phpspec describe Jcowie/HelloWorld/Model/HelloWorld what we are saying here is that we want to describe a new class called HelloWorld. Now if you followed the previous guide and you should then you will notice that I am describing code within HelloWorld yet the module we created previously was called HelloWorldModule. Now the reason is because I want to show how we can decouple framework agnostic code from code that is bound to the framework.

Lets run the spec ./bin/phpspec r select yes to get all the code generation. Lets look at the output.

~/P/M/m/src ❯❯❯ phpspec r                                                                                                                                                        master ✚ ✱ ◼

      Jcowie\HelloWorld\Model\HelloWorld

  10  ! is initializable
        class Jcowie\HelloWorld\Model\HelloWorld does not exist.

----  broken examples

        Jcowie/HelloWorld/Model/HelloWorld
  10  ! is initializable
        class Jcowie\HelloWorld\Model\HelloWorld does not exist.


1 specs
1 examples (1 broken)
19ms

  Do you want me to create `Jcowie\HelloWorld\Model\HelloWorld` for you?
                                                                         [Y/n]
Y
Class Jcowie\HelloWorld\Model\HelloWorld created in /Users/jamescowie/Projects/Magento2/magento2-docker-compose/src/app/code/Jcowie/HelloWorld/Model/HelloWorld.php.


      Jcowie\HelloWorld\Model\HelloWorld

  10  ✔ is initializable


1 specs
1 examples (1 passed)
16ms

Great so looking at the output PHPSpec has found that the class HelloWorld did not exist under app/code and it has created this for us. Lets write our first spec also known as a unit test. Open up the spec file and add in:

/**
<?php
class HelloWorldSpec extends ObjectBehavior
{
     * should return hello world
     */
    function it_should_return_hello_world()
    {
        $this->sayHello()->shouldReturn('Hello World');
    }
}

To find out more about PHPSpec and how we write specs Allan MacGregor has a great MageCast on this. But what we are saying is that when we call the method sayHello it should return us Hello World.

Running PHPSpec now ( ./bin/phpspec r) we should see some more information:

phpspec run                                                                                                                                                    ⏎ master ✚ ✱ ◼

      Jcowie\HelloWorld\Model\HelloWorld

  10  ✔ is initializable
  18  ! should return hello world
        method Jcowie\HelloWorld\Model\HelloWorld::sayHello not found.

----  broken examples

        Jcowie/HelloWorld/Model/HelloWorld
  18  ! should return hello world
        method Jcowie\HelloWorld\Model\HelloWorld::sayHello not found.


1 specs
2 examples (1 passed, 1 broken)
25ms

  Do you want me to create `Jcowie\HelloWorld\Model\HelloWorld::sayHello()`
  for you?
                                                                         [Y/n]
Y
  Method Jcowie\HelloWorld\Model\HelloWorld::sayHello() has been created.


      Jcowie\HelloWorld\Model\HelloWorld

  10  ✔ is initializable
  18  ✘ should return hello world
        expected "Hello World", but got null.

----  failed examples

        Jcowie/HelloWorld/Model/HelloWorld
  18  ✘ should return hello world
        expected "Hello World", but got null.


1 specs
2 examples (1 passed, 1 failed)
15ms

Great what is this saying. Well PHPSpec cant find the method in the class for sayHello so it has asked if we want PHPSpec to create it for us. Finally it ran the tests and all failed as the method expected us to return a string but got null. In the Red Green Refactor TDD loop its now time to write some code. Open up the Model class in HelloWorld namespace and to the method return 'Hello World'

Great so now running PHPSpec we have all passing tests but this is no good as its not actually being used in our class. Lets fix that by creating a new Command Class. Add a new folder to app/code/Jcowie/HelloWorldModule for Commands and create GenerateCommand class with the following contents:

<?php
namespace Jcowie\HelloWorldModule\Commands;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;

class GeneratorCommand extends Command
{
    /** @var \Jcowie\HelloWorld\Model\HelloWorld $helloWorldModel */
    private $helloWorldModel;

    /**
     * GeneratorCommand constructor.
     * @param \Jcowie\HelloWorld\Model\HelloWorld $helloWorld
     */
    public function __construct(\Jcowie\HelloWorld\Model\HelloWorld $helloWorld)
    {
        $this->helloWorldModel = $helloWorld;
        parent::__construct();
    }

    protected function configure()
    {
        $this->setName('jcowie:helloworld');
        $this->setDescription('Hello World');

        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln($this->helloWorldModel->sayHello());
    }
}

Most of this is just boiler plate Symfony console command code. What is interesting to us is the __construct() function where we are injecting our dependency in

/** @var \Jcowie\HelloWorld\Model\HelloWorld $helloWorldModel */
private $helloWorldModel;

/**
 * GeneratorCommand constructor.
 * @param \Jcowie\HelloWorld\Model\HelloWorld $helloWorld
 */
public function __construct(\Jcowie\HelloWorld\Model\HelloWorld $helloWorld)
{
    $this->helloWorldModel = $helloWorld;
    parent::__construct();
}

Pretty simple stuff but we inject into our class a instance of our plain ol PHP class that has the single method and assign this into a variable so that when we output data to the CLI we can call sayHello without having to create a new instance of the class. This makes testing of classes so much easier as we can if wanted test this Command class by replacing what could be more complex in the dependency injected. E.g. if its a remote webservice that we are calling. Yet I would consider this to be more of a integration test that a unit test.

To complete the module we need to add a final piece of M2 configuration that is the DI.xml file and this is used to register our command on the command registration stack

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="moduleCommand" xsi:type="object">Jcowie\HelloWorldModule\Commands\GeneratorCommand</item>
            </argument>
        </arguments>
    </type>
</config>

All that is worth noting here is that we inject into moduleCommand the reference to our GeneratorCommand so that when someone runs bin/magento our command can be added to this list. I will look at exploring the DI in lots more posts and videos soon.

What I want you to take from this most is that its really easy to de couple code from Magento 2 and create more testable objects that are not constrained by the limitations of the framework. This helps us think about Unit testing, Integration testing and Functional testing in new ways as we can have a clear seperation of our domain an inject that into smaller controllers and framework containers.

You may also find these related posts interesting: Developer Book Club All hail Xdebug and lets let var dump die A new look and a cleanup of content plus good bye github pages. Docker and Docker Sync