Yii 1.1: TDD with PHPUnit_Story and Yii

9 followers

Update 27.10.2011

Unfortunately Sebastian Bergmann decided to remove PHPUnit_Story from future PHPUnit versions. Thanks to elvan for pointing me to the relevant comment. As an alternative Behat has been suggested but it seems too bloated for me TBH. There is also PHPSpec but it's non flexible enough for me and tightly bound with English language (What's the purpose of using DSL if customers can't speak English). Since both frameworks are quite young I think I'll just stick to PHPUnit and see what's going on on the BDD frameworks arena. Even PHPUnit_Story parts of this entry are going to be deprecated soon, rest is still valid! When doing TDD, please write tests with design and behaviour of application in mind instead of testing single functions.

Why?

If you don't know what TDD is then check this out:
Test-driven developement on Wikipedia However, many people take the "Test" part of the name too seriously and they end up stuck in a place thinking "What should I test next?" instead of "What should my application do next?"

Test driven development is about a design!

Please take some time and read following articles:
Article 1
Article 2
Some people was sick of serious misunderstandings of what TDD is and what it is about (take a look what people who created TDD have to say about Microsoft's guidelines: Michael Feathers, UncleBob, Jeremy Miller) and they decided to define BDD (Behaviour driven development) which in fact is TDD which uses different wording and some Ruby and Ruby on Rails like approaches.
If you want to learn more about BDD just ask Google. The whole point of BDD is to do TDD right. Therefore I will not use name BDD, because I'm not a big fan of selling the same thing twice :). One of a few innovative things in BDD are stories. I like them and I use them a lot, because they help me focusing on actual goals I want to achieve. Watch this film: C++ Game of life TDD. It's about C++ and different testing framework, but about the same methodology and approach.

Having said all of that, I want to add that the purpose of this article is not to convince anyone to write tests or design following TDD patterns. I'm not going to change the world to what I think is good. It's just about showing how to use some tools together with Yii Framework.

Preparation

You will need a standard PHP/Yii development environment (apache, php, rdbms of some kind) additionally you will need to install newest version of PHPUnit (3.6.0RC4 at the time of writing) and some plugins:

  • PHPUnit_Story
  • DbUnit (optional for db testing)
  • PHPUnit_Selenium optional for web based functional testing)
  • PHPUnit_MockObject (optional for easy creation of stubs and mocks)

Follow installation instructions on [http://www.phpunit.de](http://www.phpunit.de/manual/current /en/installation.html ""). Just keep in mind PHPUnit 3.6 is currently in beta stage so to install it you have to add -beta after name:

pear install phpunit/PHPUnit-beta

Integration with Yii

As of version 1.1.8 Yii framework comes with good support of PHPUnit and Selenium. You should familiarize yourself with relevant parts of Yii manual. It comes with classes that overload PHPUnit classes so you can have look and feel of Yii style. Moreover, Yii adds support for fixtures. We will need 3 more classes to support PHPUnit_story extensions. They are very similar to classes which comes with Yii:

CStoryTestCase.php

<?php
/**
 * This file contains the CStoryTestCase class. Which is based on CTestCase by Qiang Xue <qiang.xue@gmail.com>, part of Yii framework <www.yiiframework.com>.
 * @author Adam "Sidewinder" Klos  <adam.klosiu@gmail.com>
 * @license http://www.yiiframework.com/license/
 */
 
require_once('PHPUnit/Runner/Version.php');
require_once('PHPUnit/Autoload.php');
 
abstract class CStoryTestCase extends PHPUnit_Extensions_Story_TestCase
{
}

CStoryDbTestCase.php

<?php
 
 /**
 * This file contains the CStoryDbTestCase class. Which is based on CDbTestCase by Qiang Xue <qiang.xue@gmail.com>, part of Yii framework <www.yiiframework.com>.
 * @author Adam "Sidewinder" Klos  <adam.klosiu@gmail.com>
 * @license http://www.yiiframework.com/license/
 */
 
abstract class CStoryDbTestCase extends CStoryTestCase
{
    /**
     * @var array a list of fixtures that should be loaded before each test method executes.
     * The array keys are fixture names, and the array values are either AR class names
     * or table names. If table names, they must begin with a colon character (e.g. 'Post'
     * means an AR class, while ':Post' means a table name).
     * Defaults to false, meaning fixtures will not be used at all.
     */
    protected $fixtures=false;
 
    /**
     * PHP magic method.
     * This method is overridden so that named fixture data can be accessed like a normal property.
     * @param string $name the property name
     * @return mixed the property value
     */
    public function __get($name)
    {
        if(is_array($this->fixtures) && ($rows=$this->getFixtureManager()->getRows($name))!==false)
            return $rows;
        else
            throw new Exception("Unknown property '$name' for class '".get_class($this)."'.");
    }
 
    /**
     * PHP magic method.
     * This method is overridden so that named fixture ActiveRecord instances can be accessed in terms of a method call.
     * @param string $name method name
     * @param string $params method parameters
     * @return mixed the property value
     */
    public function __call($name,$params)
    {
        if(is_array($this->fixtures) && isset($params[0]) && ($record=$this->getFixtureManager()->getRecord($name,$params[0]))!==false)
            return $record;
        else
            return parent::__call($name,$params);
    }
 
    /**
     * @return CDbFixtureManager the database fixture manager
     */
    public function getFixtureManager()
    {
        return Yii::app()->getComponent('fixture');
    }
 
    /**
     * @param string $name the fixture name (the key value in {@link fixtures}).
     * @return array the named fixture data
     */
    public function getFixtureData($name)
    {
        return $this->getFixtureManager()->getRows($name);
    }
 
    /**
     * @param string $name the fixture name (the key value in {@link fixtures}).
     * @param string $alias the alias of the fixture data row
     * @return CActiveRecord the ActiveRecord instance corresponding to the specified alias in the named fixture.
     * False is returned if there is no such fixture or the record cannot be found.
     */
    public function getFixtureRecord($name,$alias)
    {
        return $this->getFixtureManager()->getRecord($name,$alias);
    }
 
    /**
     * Sets up the fixture before executing a test method.
     * If you override this method, make sure the parent implementation is invoked.
     * Otherwise, the database fixtures will not be managed properly.
     */
    protected function setUp()
    {
        parent::setUp();
        if(is_array($this->fixtures))
            $this->getFixtureManager()->load($this->fixtures);
    }
}

Update 20.10.2011

It seems like there is a serious problem with implementation of stories and selenium integration in PHPUnit_Extensions_Story_SeleniumTestCase. Also It seems like in newest beta of PHPUnit-selenium support for stories is dropped. Hopefully it's just temporary. You don't have to bother with creating CStoryWebTestCase couse it will not work as intended anyway. For now use CWebTestCase Yii comes with. I will update as soon as I will have any info.

CStoryWebTestCase.php

<?php
 /**
 * This file contains the CStoryWebTestCase class. Which is based on CWebTestCase by Qiang Xue <qiang.xue@gmail.com>, part of Yii framework <www.yiiframework.com>.
 * @author Adam "Sidewinder" Klos  <adam.klosiu@gmail.com>
 * @license http://www.yiiframework.com/license/
 */
 
require_once('PHPUnit/Extensions/Story/SeleniumTestCase.php');
 
abstract class CStoryWebTestCase extends PHPUnit_Extensions_Story_SeleniumTestCase
{
    /**
     * @var array a list of fixtures that should be loaded before each test method executes.
     * The array keys are fixture names, and the array values are either AR class names
     * or table names. If table names, they must begin with a colon character (e.g. 'Post'
     * means an AR class, while ':Post' means a table name).
     * Defaults to false, meaning fixtures will not be used at all.
     */
    protected $fixtures=false;
 
    /**
     * PHP magic method.
     * This method is overridden so that named fixture data can be accessed like a normal property.
     * @param string $name the property name
     * @return mixed the property value
     */
    public function __get($name)
    {
        if(is_array($this->fixtures) && ($rows=$this->getFixtureManager()->getRows($name))!==false)
            return $rows;
        else
            throw new Exception("Unknown property '$name' for class '".get_class($this)."'.");
    }
 
    /**
     * PHP magic method.
     * This method is overridden so that named fixture ActiveRecord instances can be accessed in terms of a method call.
     * @param string $name method name
     * @param string $params method parameters
     * @return mixed the property value
     */
    public function __call($name,$params)
    {
        if(is_array($this->fixtures) && isset($params[0]) && ($record=$this->getFixtureManager()->getRecord($name,$params[0]))!==false)
            return $record;
        else
            return parent::__call($name,$params);
    }
 
    /**
     * @return CDbFixtureManager the database fixture manager
     */
    public function getFixtureManager()
    {
        return Yii::app()->getComponent('fixture');
    }
 
    /**
     * @param string $name the fixture name (the key value in {@link fixtures}).
     * @return array the named fixture data
     */
    public function getFixtureData($name)
    {
        return $this->getFixtureManager()->getRows($name);
    }
 
    /**
     * @param string $name the fixture name (the key value in {@link fixtures}).
     * @param string $alias the alias of the fixture data row
     * @return CActiveRecord the ActiveRecord instance corresponding to the specified alias in the named fixture.
     * False is returned if there is no such fixture or the record cannot be found.
     */
    public function getFixtureRecord($name,$alias)
    {
        return $this->getFixtureManager()->getRecord($name,$alias);
    }
 
    /**
     * Sets up the fixture before executing a test method.
     * If you override this method, make sure the parent implementation is invoked.
     * Otherwise, the database fixtures will not be managed properly.
     */
    protected function setUp()
    {
        parent::setUp();
        if(is_array($this->fixtures))
            $this->getFixtureManager()->load($this->fixtures);
    }
}

Now, copy all 3 files to protected\components\PHPUnit_story directory and edit your test configuration file to import:

'application.components.PHPUnit_story.*'

That's all! You should be able to write tests in form of stories by extending classes you just copied to your application directory.

Running Tests

Be sure you read this section of PHPUnit manual.
Now create a new file named storyTest.php under protected\tests\unit directory and paste following contents there:

<?php
 
class BowlingGameSpec extends CStoryTestCase
{
    /**
     * @scenario
     */
    public function scoreForGutterGameIs1()
    {
        $this->given('New game')
             ->then('Score should be', 1);
    }
    /**
    * @scenario
    */
    public function scoreForGutterGameIs0()
    {
        $this->given('New game')
             ->then('Score should be', 0);
    }
 
    public function runGiven(&$world, $action, $arguments)
    {
        switch($action) {
            case 'New game': {
                $world['rolls'] = 0;
            }
            break;
 
            default: {
                return $this->notImplemented($action);
            }
        }
    }
 
    public function runWhen(&$world, $action, $arguments)
    {
        switch($action) {
 
            default: {
                return $this->notImplemented($action);
            }
        }
    }
 
    public function runThen(&$world, $action, $arguments)
    {
        switch($action) {
            case 'Score should be': {
                $this->assertEquals($arguments[0], 0);
            }
            break;
 
            default: {
                return $this->notImplemented($action);
            }
        }
    }
}

above test is brutal rip of test presented in PHPUnit_story manual and it's only purpose is to check if everything is working.
Open command line window, navigate to protected\tests directory and run following command:

phpunit --printer PHPUnit_Extensions_Story_ResultPrinter_Text unit\storyTest

you should see something like that:

PHPUnit 3.6.0RC4 by Sebastian Bergmann.

Configuration read from D:\xampp\htdocs\yii_apps\tgw\app\tests\phpunit.xml

BowlingGameSpec
 [ ] Score for gutter game is 1

   Given New game
    Then Score should be 1

 [x] Score for gutter game is 0

   Given New game
    Then Score should be 0

Scenarios: 2, Failed: 1, Skipped: 0, Incomplete: 0.

In case you get errors, check if you installed everything and your files names (including case) are correct.

What now?

Remember that 'units' PHPUnit refers to, don't have to be classes! So don't try to test your model only. Write a story of user adding comment to your blog and check it with selenium, write a story of administrator changing user's password and check it. Test functionality and behaviours not single methods. Don't try to test private methods, they will be tested with public methods, if not they are not necessary! Don't test Yii Framework, it's already tested by Yii authors and contributors, check your code only.

If you have any comments, I'm ready for critics :). Also, I'm not native English speaker, so any corrections are welcome.

That's all.

Total 1 comment

#5568 report it
raysto at 2011/10/20 08:41pm
TDD & Yii

Great article. TDD is where it's at. We made testing the centerpiece of our app built on Yii (zurmo.org). We have over 1000+ unit tests and growing daily. We utilize selenium as well to have a nice set of functional tests.

Leave a comment

Please to leave your comment.

Write new article