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
{
protected $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);
}
Found a typo or you think this page needs improvement?
Edit it on github !
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(); }
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));
.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', );
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.
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
Signup or Login in order to comment.