A Guide to Running Unit Test using PHPUnit on Yii Framework

Let me tell you something:

TDD doesn’t drive towards good design, it drives away towards bad design. If you know what good design is, the result is a better design – Nat Pryce

I want to share my experience in setting up unit test (and later integrate it in CI —Continuous Integration) on my development machine. Hope this will save your time, my fellow developers, without going through of all errors and warnings I was stuck with before.

Disclaimer
I’m not an expert on Yii even though I have years of experience in PHP. In fact, this is the first time I’m using Yii. In the real world where I live, work and play, I use CodeIgniter and CakePHP. In terms of TDD, I started using it almost 2 years ago (thanks to Yahoo!) and always using it in almost every projects including Android (Java) and Python.

FYI, I’m using MacBookPro with MAMP PRO (PHP 5.3.6) installed. It wouldn’t be too difficult for you if you use Linux since it’s a similar process. If you use Windows, well, then goodluck! ;)

Let’s get started.

Installing PHPUnit

First, you need to install PHPUnit. If you have already installed PHPUnit, then skip to the next step. How do you know that your system already has PHPUnit? One must check for its existence:

$ cd /Applications/MAMP/bin/php/php5.3.6
$ find . -name phpunit

change /Applications/MAMP/bin/php/php5.3.6 with your own PHP installation. Read the documentation along with your system to find out. If you’re lucky, your system will return this kind of line:

./bin/phpunit

which means your system already has phpunit. Else, just install it.

Run this within your current directory where PEAR installed (usually it’s in the same location with php installation).

$ sudo pear config-set auto_discover 1
$ sudo ./pear install --alldeps pear.phpunit.de/PHPUnit

and watch the magic begin.

Please noted that you’re installing the most-updated version of PHPUnit (3.7.7) and while it’s good, I prefer to downgrade to 3.7.1. This is why.

Installing Selenium

$ sudo ./pear install phpunit/PHPUnit_Selenium
downloading PHPUnit_Selenium-1-3-2.tgz...
Starting to download PHPUnit_Selenium-1.3.2.tgz (44,236 bytes)
............done: 44,236 bytes
install ok: channel://pear.phpunit.de/PHPUnit_Selenium-1.3.2
$

Now you have Selenium installed. Good!

Configuring Yii Framework

It’s not that hard. Just go to protected/tests directory on your Yii framework and try to run this command:

$ phpunit .

if you got the -bash: phpunit: command not found error, then you need to include the phpunit in your path.

If everything okay, you will get this message:

PHPUnit 3.7.1 by Sebastian Bergmann.
Configuration read from /Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/phpunit.xml

SSSSSS.

Time: 228 ms, Memory: 8.75Mb

OK, but incomplete or skipped tests!
Tests: 7, Assertions: 4, Skipped: 6.

You’re now ready to create your first unit test.

wait, wait…
in case you’re wondering what was that, let me give a brief explanation. But, hey, where’s the fun? I’ll explain to you when we get there. Hang on ;)

Time to Play

OK. Now let’s say you want to create a test for one of your classes. Since my Yii has been modified to fit my needs, so I have a slightly different structure from yours, but that’s not a problem. Here’s the thing:

Step 1
Create your unit test class under the protected/tests/unit folder and name it the same as your class name you want to test, adding a ‘Test’ word after it.

In my case, I will create a file named ApiControllerTest.php that contains all tests for ApiController.php class.

Step 2
If your original class (not the class with ‘Test’ one) is not in the protected/components directory, copy the original file to this directory or better create a new class that extends the original class. This way will save you from maintaining two different base code. Don’t forget to require_once() the original class inside the new class.

My case is that the ApiController.php class is located somewhere outside the protected/components directory so I created a new class named ApiControllerT.php that extends ApiController.php.

Here is what the ApiControllerT.php looks like:

<?php

require_once(dirname(__FILE__) . '/../../../backend/controllers/ApiController.php');

class ApiControllerT extends ApiController {
}

If you understand OOP enough and not skipping the inheritance subject when you learned about it, you might understand this part without problem.

Step 3
Open your ApiControllerTest.php unit test class in step #1 above and make it something similar like this (based on your requirement and structure):

class ApiControllerTest extends CTestCase {

    public function setUp() {
        $this->api = new ApiController(rand());
    }

    public function tearDown() {
        unset($this->api);
    }
}

setUp() function is used to, well, setup everything before the test get started while tearDown() function will destroy any object we use in the test to clean everything up.

Step 4
Let’s try to test one single method in my ApiController.php, that is formatResponseHeader. This is what it is doing.

public function formatResponseHeader($code) {
    if (!array_key_exists($code, $this->response_code)) {
        $code = '400';
    }

    return 'HTTP/1.1 ' . $code . ' ' . $this->response_code[$code];
}

As you might expected, this method will set the header’s response code based on the parameter accepted.

Now, to test this method, I’ll open ApiControllerTest.php and add this code below after setUp() and before tearDown() methods.

public function testFormatResponseHeader() {
    $this->assertEquals('HTTP/1.1 400 Bad Request', $this->api->formatResponseHeader('400'));
    $this->assertEquals('HTTP/1.1 200 OK', $this->api->formatResponseHeader('200'));
    $this->assertEquals('HTTP/1.1 400 Bad Request', $this->api->formatResponseHeader('500'));
    $this->assertNotEquals('HTTP/1.1 304 Not Modified', $this->api->formatResponseHeader('204'));
}

If you don’t understand the code above, please refer to this resource.

Save the change in ApiControllerTest.php and then try to run this in protected/tests directory:

$ phpunit .

and, if everything works, you will get the same result as before.

So what’s up with the result? Let me copy it here.

PHPUnit 3.7.1 by Sebastian Bergmann.
Configuration read from /Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/phpunit.xml

SSSSSS.

Time: 228 ms, Memory: 8.75Mb

OK, but incomplete or skipped tests!
Tests: 7, Assertions: 4, Skipped: 6.

As you might guess, S means skipped test while .(dot) indicate successful or passed test. Time indicates how long the test was running and Memory indicates how much memory used for the test.

If there are some errors or failed test, it will show like this (for errors):

PHPUnit 3.7.1 by Sebastian Bergmann.

Configuration read from /Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/phpunit.xml

SSSSSSE

Time: 82 ms, Memory: 8.75Mb

There was 1 error:

1) ApiControllerTest::testFormatResponseHeader
CException: ApiControllerT and its behaviors do not have a method or closure named "formatResponseHeaders".

/Applications/MAMP/htdocs/foo/bar/common/lib/Yii/base/CComponent.php:266
/Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/unit/ApiControllerTest.php:13
/Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/unit/ApiControllerTest.php:13
/Applications/MAMP/bin/php/php5.3.6/bin/phpunit:46

FAILURES!
Tests: 7, Assertions: 3, Errors: 1, Skipped: 6.

or this (for failed test):

PHPUnit 3.7.1 by Sebastian Bergmann.

Configuration read from /Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/phpunit.xml

SSSSSSF

Time: 60 ms, Memory: 8.75Mb

There was 1 failure:

1) ApiControllerTest::testFormatResponseHeader
Failed asserting that 'HTTP/1.1 400 Bad Request' is not equal to <string:HTTP/1.1 400 Bad Request>.

/Applications/MAMP/htdocs/foo/bar/common/lib/Yii/cli/views/webapp/protected/tests/unit/ApiControllerTest.php:12
/Applications/MAMP/bin/php/php5.3.6/bin/phpunit:46

FAILURES!
Tests: 7, Assertions: 3, Failures: 1, Skipped: 6.

That’s it! Now you’re ready to write more tests on your Yii-based app. Remember, TDD doesn’t drive towards good design but it drives away towards bad design ;)

Comments and suggestion on how to make it better are welcome since I’m new to Yii :)

Also read...

Comments

  1. Pingback: A Guide to Running Unit Test using PHPUnit on Yii Framework | Rocketboom

  2. Pingback: unit test with yii | gabconcepcion

  3. i’m suggesting not using

    require_once(dirname(__FILE__) . ‘/../../../backend/controllers/ApiController.php’);

    you can use Yii import
    Yii::import(‘application.componetns.backend.controllers.ApiController’);

    or namespace

    use \application\components\backend\controllers\ApiController;

    Reply
    • Yes, you’re right. I’ve been aware of this when I used ‘require_once’ and had a feeling that better methods exist. Thanks for pointing that. I’ve changed the code to use Yii::import few days ago and will update the correction once my post about integrating it using CI is published.

      Reply
      • and once Yii supported it, I began using namespace instead of Yii import. The underlying calling mechanism is not very different than Yii import actually, but it’s much more readable and more IDE-friendly.

        Reply
        • Well, I’d prefer Yii::import since it’s more specific and you don’t have to keep track on what namespace your class is in. Yii::import allows you to only use one specific class and never get distracted by namespace. Either way, those are better than using require or require_once. Just my 2 cents!

          Reply
  4. most n00b programmers don’t care about this unit testing shit. Once you’re using it, you’ll regret why people ignore it. it always goes like that.

    Reply
  5. I got response like bwlow, when i run phpunit .
    was this ok..?
    I installed phpunit & selenium versions as you mentioned ANd php version is 5.4.28.

    class UserIdentityTest extends CTestCase {

    public function setUp() {
    $this->api = new UserIdentity(rand());
    }

    public function testFormatResponseHeader() {
    $this->assertEquals(‘HTTP/1.1 400 Bad Request’, $this->api->formatResponseHeader(‘400’));
    $this->assertEquals(‘HTTP/1.1 200 OK’, $this->api->formatResponseHeader(‘200’));
    $this->assertEquals(‘HTTP/1.1 400 Bad Request’, $this->api->formatResponseHeader(‘500’));
    $this->assertNotEquals(‘HTTP/1.1 304 Not Modified’, $this->api->formatResponseHeader(‘204’));
    }

    public function tearDown() {
    unset($this->api);
    }
    }
    PHPUnit 3.7.1 by Sebastian Bergmann.

    Time: 0 seconds, Memory: 0.75Mb

    No tests executed!

    Reply
  6. Hi
    Currently i am learning yii but at that level i want detail how to develop front back with api structure with all testing unit other one please guide me

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *