Unit Testing

Because the Yii testing framework is built on top of PHPUnit, it is recommended that you go through the PHPUnit documentation first to get the basic understanding on how to write a unit test. We summarize in the following the basic principles of writing a unit test in Yii:

  • A unit test is written in terms of a class XyzTest which extends from CTestCase or CDbTestCase, where Xyz stands for the class being tested. For example, to test the Post class, we would name the corresponding unit test as PostTest by convention. The base class CTestCase is meant for generic unit tests, while CDbTestCase is suitable for testing active record model classes. Because PHPUnit_Framework_TestCase is the ancestor class for both classes, we can use all methods inherited from this class.

  • The unit test class is saved in a PHP file named as XyzTest.php. By convention, the unit test file may be stored under the directory protected/tests/unit.

  • The test class mainly contains a set of test methods named as testAbc, where Abc is often the name of the class method to be tested.

  • A test method usually contains a sequence of assertion statements (e.g. assertTrue, assertEquals) which serve as checkpoints on validating the behavior of the target class.

In the following, we mainly describe how to write unit tests for active record model classes. We will extend our test classes from CDbTestCase because it provides the database fixture support that we introduced in the previous section.

Assume we want to test the Comment model class in the blog demo. We start by creating a class named CommentTest and saving it as protected/tests/unit/CommentTest.php:

class CommentTest extends CDbTestCase
{
    public $fixtures=array(
        'posts'=>'Post',
        'comments'=>'Comment',
    );
 
    ......
}

In this class, we specify the fixtures member variable to be an array that specifies which fixtures will be used by this test. The array represents a mapping from fixture names to model class names or fixture table names (e.g. from fixture name posts to model class Post). Note that when mapping to fixture table names, we should prefix the table name with a colon (e.g. :Post) to differentiate it from model class name. And when using model class names, the corresponding tables will be considered as fixture tables. As we described earlier, fixture tables will be reset to some known state each time when a test method is executed.

Fixture names allow us to access the fixture data in test methods in a convenient way. The following code shows its typical usage:

// return all rows in the 'Comment' fixture table
$comments = $this->comments;
// return the row whose alias is 'sample1' in the `Post` fixture table
$post = $this->posts['sample1'];
// return the AR instance representing the 'sample1' fixture data row
$post = $this->posts('sample1');

Note: If a fixture is declared using its table name (e.g. 'posts'=>':Post'), then the third usage in the above is not valid because we have no information about which model class the table is associated with.

Next, we write the testApprove method to test the approve method in the Comment model class. The code is very straightforward: we first insert a comment that is pending status; we then verify this comment is in pending status by retrieving it from database; and finally we call the approve method and verify the status is changed as expected.

public function testApprove()
{
    // insert a comment in pending status
    $comment=new Comment;
    $comment->setAttributes(array(
        'content'=>'comment 1',
        'status'=>Comment::STATUS_PENDING,
        'createTime'=>time(),
        'author'=>'me',
        'email'=>'me@example.com',
        'postId'=>$this->posts['sample1']['id'],
    ),false);
    $this->assertTrue($comment->save(false));
 
    // verify the comment is in pending status
    $comment=Comment::model()->findByPk($comment->id);
    $this->assertTrue($comment instanceof Comment);
    $this->assertEquals(Comment::STATUS_PENDING,$comment->status);
 
    // call approve() and verify the comment is in approved status
    $comment->approve();
    $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
    $comment=Comment::model()->findByPk($comment->id);
    $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
}
$Id$

Total 5 comments

#16356 report it
Neoc83 at 2014/02/14 03:40am
Table prefixes and referring to the fixture data

When using a fixture with a table prefix like:

protected $fixtures = array(
    '{{post}}' => 'Post',
);

and you want to refer to the fixture data in the test cases I found that it's not possible to use the following code:

$this->post('sample1');

To make it work I wrote a class DbTestCase that extends CDbTestCase and overrides the PHP magic methods __get and __call. Please find the result and how to use it here: Related forum post

#13093 report it
Albert St Clair at 2013/05/03 08:58pm
function testApprove
$comment->setAttributes(array(
    'content'=>'comment 1',
    'status'=>Comment::STATUS_PENDING,
    'create_time'=>time(),
    'author'=>'me',
    'email'=>'me@example.com',
    'post_id'=>$this->posts['sample1']['id'],
),false);

the create_time and post_id keys should not be camelCased.

#11797 report it
Andres Felipe Diaz at 2013/02/04 12:28pm
Name of the fixtures file

Make sure that the file name of the fixture is the table name including the prefix (i.e: tbl_posts.php).

Else, you will get an error saying that the property is not part of the Test Case Class when executing the following code:

$this->posts('sample1');

Also, take on account that when defining the fixture property in the Test Case class, do it without the prefix.

public $fixtures=array(
        'posts'=>'Post',
    );
#7960 report it
François Gannaz at 2012/04/29 08:19am
CWebApplication in unit tests

When you launch unit tests, the Yii application wil still be an instance of CWebApplication, but its initial attributes are very different. For instance, if you try to test a method that uses Yii::app()->controller, then PHPUnit will crash.

I needed to test a method that returned HTML links. So I had to complete by myself the application so that link generation would be possible. I added to my test class:

public function setUp()
{
    // initialize a controller (which defaults to null in tests)
    $c = new CController('phpunit');
    $c->setAction(new CInlineAction($c, 'urltest'));
    Yii::app()->setController($c);
}

Then I could test code that contained Yii::app()->getController()->createUrl('', array('id' => $id));.

#5789 report it
Asgaroth at 2011/11/13 07:39pm
Call parent implementation

If for some reason you need to override the "setup" method in you test class, say to initialize (or simulate) session data like Yii::app()->user->id.

Dont forget to call the parent implementation, or you'll get some weeeeird errors about missing properties/methods in your class:

public function setUp(){
        Yii::app()->user->id = 1;
        parent::setUp();
 }

Leave a comment

Please to leave your comment.