Skip to content

crowdint/phpspec-magento-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CrowdPHPSpecTraining

At Crowd Interactive, we are interested in testing all our code and Magento is not the exception. We are implementing the MageTest/MageSpec Module based on PHPSpec toolset.

In this implementation, we have two different tests taken from the Magecasts tutorials. The first one is to understand how PHPSpec works and the second is to show how MageSpec works.

First Test

"We will look at PHPspec and how we can use it to enhance our development workflow".

Prerequisites

PHP 5.3.x or greater
Install composer

Getting Started

Create composer.json file

On root project, create composer.json file with the data below:

{
	"require-dev": {
		"phpspec/phpspec": "~2.1"
	},
	"config": {
		"bin-dir": "bin"
	},
    "autoload": {
        "psr-0": {
             "Crowd\\Store": "src"
         }
    }
}

Run composer install

$ composer install

You will get something like this:

Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing doctrine/instantiator (1.0.5)
    Loading from cache

  - Installing symfony/yaml (v2.7.2)
    Loading from cache

  - Installing symfony/process (v2.7.2)
    Loading from cache

  - Installing symfony/finder (v2.7.2)
    Loading from cache

  - Installing symfony/event-dispatcher (v2.7.2)
    Loading from cache

  - Installing symfony/console (v2.7.2)
    Loading from cache

  - Installing sebastian/recursion-context (1.0.0)
    Loading from cache

  - Installing sebastian/exporter (1.2.0)
    Loading from cache

  - Installing phpspec/php-diff (v1.0.2)
    Loading from cache

  - Installing sebastian/diff (1.3.0)
    Loading from cache

  - Installing sebastian/comparator (1.1.1)
    Loading from cache

  - Installing phpdocumentor/reflection-docblock (2.0.4)
    Loading from cache

  - Installing phpspec/prophecy (v1.4.1)
    Loading from cache

  - Installing phpspec/phpspec (2.2.1)
    Loading from cache

symfony/event-dispatcher suggests installing symfony/dependency-injection ()
symfony/event-dispatcher suggests installing symfony/http-kernel ()
symfony/console suggests installing psr/log (For using the console logger)
phpdocumentor/reflection-docblock suggests installing dflydev/markdown (~1.0)
phpdocumentor/reflection-docblock suggests installing erusev/parsedown (~1.0)
phpspec/phpspec suggests installing phpspec/nyan-formatters (~1.0 – Adds Nyan formatters)

PHPSpec run command

Now that we have bin/ and vendor/ directory in our project, let's go with this:

$ bin/phpsepc run

And you will see:

0 specs
0 examples
0ms

Play with MageSpec and PHPSpec

$ bin/phpspec describe Crowd/Store/Product

Will return this message:

Specification for Crowd\Store\Product created in [rootproject]/spec/Crowd/Store/ProductSpec.php

In your folder project now you will see

![spec folder] (http://i.imgur.com/p9uu1TX.png)

The content of the ProductSpec.php created file looks like this:

<?php

namespace spec\Crowd\Store;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ProductSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Crowd\Store\Product');
    }
}
$ bin/phpspec run

And now you will see this:

![class does not exists] (http://i.imgur.com/0iZ7OV5.png)

Obviously you type YES and you will get the next message:

![class was created] (http://i.imgur.com/r4oTSWn.png)

And the next file structure in your project

![src file structure] (http://i.imgur.com/4DAdc3t.png)

Inside Product.php you will see

<?php

namespace Crowd\Store;

class Product
{
}

Now inside [rootproject]/spec/Crowd/Store/ProductSpec.php file, write the next functions

function it_should_have_a_name()
{
    $this->getName()->shouldReturn('Testing Spec');
}

function it_should_have_sku()
{
    $this->getSku()->shouldReturn('12345');
}

The complete files looks like:

<?php

namespace spec\Crowd\Store;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ProductSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Crowd\Store\Product');
    }

    function it_should_have_a_name()
    {
        $this->getName()->shouldReturn('Testing Spec');
    }

    function it_should_have_sku()
    {
        $this->getSku()->shouldReturn('12345');
    }
}

What are we doing here? Well, we are defining two more test functions, one to check if a function called getName() returns a string with value "Testing Spec" and other to check if a function called getSku() returns a string with value "12345"

On terminal lets do:

$ bin/phpspec run

It will display:

![getName() function does not exists] (http://i.imgur.com/aT7yySp.png)

Press Y + return key:

![getSku() function does not exists] (http://i.imgur.com/6KUmIbi.png)

Press Y + return key again:

![functions exists but tests does not pass] (http://i.imgur.com/eOieqj8.png)

What does this mean? First of all, PHPSpec checks if getName() and getSku() functions exists in the Product class, and if it does not find them, asks us if we want to crete them. Once created, PHPSpec will try to run the tests, but they do not pass.

The Product.php file under [rootproject]/src/Crowd/Store/ changed its content and now looks like:

<?php

namespace Crowd\Store;

class Product
{

    public function getName()
    {
        // TODO: write logic here
    }

    public function getSku()
    {
        // TODO: write logic here
    }
}

That's the reason of out tests not passing: the functions exist, but they do nothing. Lets work with them and type the following in Product.php

<?php

namespace Crowd\Store;

class Product
{

    protected $_name;
    protected $_sku;

    public function __construct($name = '', $sku = '')
    {
        $this->_name    = $name;
        $this->_sku     = $sku;
    }

    public function getName()
    {
        return $this->_name;
    }

    public function getSku()
    {
        return $this->_sku;
    }
}

Run the specs again:

$ bin/phpspec run

And... oh!, they don't pass again. Why?

![test not pass again] (http://i.imgur.com/4ODyU1i.png)

This happened because phpspec is running without any init data. To do that, let's type the following inside ProductSpec class:

function let()
{
    $name   = "Testing Spec";
    $sku    = "12345";
    $this->beConstructedWith($name, $sku);
}

The let() method is used to pass data into the constructor each time the parser gets created using the beConstructedWith() method

Now on the console, type once again

$ bin/phpspec run

![test pass] (http://i.imgur.com/YEOFaUC.png)

And we did it, friends!.

Second Test

In this case we are going to show how you can test your Magento modules using MageSpec

We are going to need a Magento installation inside of our root project.

Getting Started

Update composer.json

We have to update our composer.json file with the below data

{
	"require-dev": {
		"psy/psysh": "@stable",
        "magetest/magento-phpspec-extension": "~2.0"
	},
	"config": {
		"bin-dir": "bin"
	},
    "autoload": {
        "psr-0": {
             "": [
                "magento/app",
                "magento/app/code/local",
                "magento/app/code/community",
                "magento/app/code/core",
                "magento/lib"
             ]
         }
    },
    "minimum-stability": "dev"
}

That json file contains the following changes:

  1. The require dev modules are "psy/psysh" and "magetest/magento-phpspec-extension" intead of "phpspec/phpspec".
  2. The autoload field is used to load all the Magento files. As you see, a Magento installation called "magento" exist inside our root project.

After type on terminal this

$ composer update

The configuration file

Once composer update all the dependecies we have to create a configuration file called phpspec.yml in our root folder, this file will be used for PHPSpec to load the Magento Extension, the contains of this file is:

extensions: [MageTest\PhpSpec\MagentoExtension\Extension]
mage_locator:
  spec_prefix: 'spec'
  src_path: 'magento/app/code'
  spec_path: 'spec/app/code'
  code_pool: 'local'

What the file structure means?

  • [extensions]: Who is the extension required
  • [src_path]: The path to store the generated classes. MageSpec creates the directories if they do not exist.
  • [spec_path]: The path of the specifications.
  • [code_pool]: The code pool to store the Magento module.

Now we can run the following command:

$ bin/phpspec

And you will see something like this:

bin/phpspec with MageSpec

Let's play with MageSpec

As you see in the picture above there are some commands availables to work with PHPSpec, but also MageSpec has some useful description paramaters for the purposes of Magento.

We are going to work with the describe:block command. Then below we type on terminal:

$ bin/phpspec describe:block crowd_helloworld/message

It should generate the following:

Specification for Crowd_Helloworld_Block_Message created in [rootproject]spec/app/code/local/Crowd/Helloworld/Block/MessageSpec.php.

And you spec folder will look like:

spec local folder

The MessageSpec.php file contains:

<?php

namespace spec;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class Crowd_Helloworld_Block_MessageSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Crowd_Helloworld_Block_Message');
    }
}

Now, type on terminal:

$ bin/phpspec run

You will see a message like this:

no class exist

Just as in the previous article, the message above occurs becouse the class does not exist, and MageSpec ask us if we wanto to creat it. Type yes of course and you will see a message like this:

helloworld_message class does not exist

Now inside your magento/app/ folder the following structure exist:

local app folders

The contain of the files creted are:

config.xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <Crowd_Helloworld>
            <version>0.1.0</version>
        </Crowd_Helloworld>
    </modules>
    <global>
        <blocks>
            <crowd_helloworld>
                <class>Crowd_Helloworld_Block</class>
            </crowd_helloworld>
        </blocks>
    </global>
</config>
Message.php
<?php

class Crowd_Helloworld_Block_Message extends Mage_Core_Block_Abstract
{

}

This is very great, becouse it also create for us the configuration structure for a Magento module with a blocks class declaration, in this case by the "describe:block" command.

Inside of [rootproject]/spec/app/code/local/Crowd/Helloworld/Block/MessageSpec.php type the following method to catch a message thtat returns frome the message() method in the Crowd_Helloworld_Block_Message class:

function it_should_tell_you_that_you_must_be_registered(){
	$this->message()->shouldReturn('Hello guest, Please register with us for special offers');
}

Complete file looks like:

<?php

namespace spec;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class Crowd_Helloworld_Block_MessageSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Crowd_Helloworld_Block_Message');
    }

    function it_should_tell_you_that_you_must_be_registered(){
        $this->message()->shouldReturn('Hello guest, Please register with us for special offers');
    }
}

On terminal, run phpspec again

$ bin/phpspec run

You will see that exist to test methods to run, but just one of them pass, this is becouse an exception occurs looking for the message() inside of the block class.

message method does not exist

Type the message() function in the Crowd_Helloworld_Block_Message class, the entire class will look like this:

<?php

class Crowd_Helloworld_Block_Message extends Mage_Core_Block_Abstract
{

    public function message(){}
}

Run phpspec again an it will not pass, becouse the method exist but it not returns the right message

not message

Edit the file again to add a return message text inside the message() method, and the complete file will look like:

<?php

class Crowd_Helloworld_Block_Message extends Mage_Core_Block_Abstract
{

    public function message(){
        return 'Hello guest, Please register with us for special offers';
    }
}

Run the phpspec commando to test and see what happened:

first method test pass

That works, and is ok, we tested what should be the message for guest users, but what happened with registerd users yet. Update the MessageSpec.php file with the following code.

<?php

namespace spec;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class Crowd_Helloworld_Block_MessageSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Crowd_Helloworld_Block_Message');
    }

    function it_should_tell_you_that_you_must_be_registered(){
        $this->message()->shouldReturn('Hello guest, Please register with us for special offers');
    }

    function it_should_tell_you_a_welcome_message()
    {
        $this->message()->shouldReturn('Hello registered user');
    }
}

We've added the it_should_tell_you_a_welcome_message() test method that is looking for a "Hello registered user" return message in the same message() method.

Run again the phpspec command and see wath happened:

one pass but other not

One of our test still working but the new does not work, why? Becouse our message method just return one message, we forget some logic to make possible have both message depending of the state of the current user.

Let's create an adapter class to mock the Magento core code.

Your Magento folder structure will look like:

magento module with adapter

In the State.php file add the following code:

<?php

class Crowd_Helloworld_Adapter_State {

    public function isLoggedIn(){

        if(Mage::getSingleton('customer/session')->isLoggedIn()){
            return true;
        } else {
            return false;
        }
    }
}

We will use the isLoggedIn() method after to check what happened if the user is logged in or not. Now in our Magento module block class define an private variable add the construct method at the beginning with the following data, this code with help us to mock the adapter and get the results we want without having to check the session/database:

private $_stateAdapter;

public function __construct(array $services = array()){
	if (isset($services['state_adapter'])) {
	    $this->_stateAdapter = $services['state_adapter'];
	}
	if (!$this->_stateAdapter instanceof Crowd_Helloworld_Adapter_State) {
	    $this->_stateAdapter = new Crowd_Helloworld_Adapter_State();
	}
}

Now update the message method at the same class to looks like:

public function message(){
	$state = $this->_stateAdapter;
	
	if($state->isLoggedIn()){
	    return 'Hello registered user';
	} else {
	    return 'Hello guest, Please register with us for special offers';
	}
}

Do you remember the let() method from previous article, we are going to use it again to construct our tests with the adapter data. The function let will look like:

function let(\Crowd_Helloworld_Adapter_State $adapter)
{
	$this->beConstructedWith(array('state_adapter' => $adapter));
}

And the last thing to do is update out test methods to looks like this:

function it_should_tell_you_that_you_must_be_registered($adapter){
	$adapter->isLoggedIn()->willReturn(false);
	$this->message()->shouldReturn('Hello guest, Please register with us for special offers');
}

function it_should_tell_you_a_welcome_message($adapter)
{
	$adapter->isLoggedIn()->willReturn(true);
	$this->message()->shouldReturn('Hello registered user');
}

On terminal type phpspec run command onece again and it will work.

complete testing