因为Yii测试框架基于PHPUnit构建,所以推荐你在理解如何写一个单元测试之前先通读一遍PHPUnit文档。下面我们简要概括一下在Yii中写一个单元测试的基本原则:

  • 一个单元测试以继承自CTestCase或者CDbTestCase的XyzTest类的形式编写, 其中Xyz代表要被测试的类. 例如, 想要测试Post类,我们会相应地将测试类命名为PostTest. 基类CTestCase是通用单元测试类, 而CDbTestCase只适用于测试AR模型类. 由于PHPUnit_Framework_TestCase是这两个类的父类, 我们可以从这个类中继承所有方法。
  • 单元测试类以XyzTest.php的形式保存在PHP文件中. 方便起见,单元测试文件通常保存在 protected/tests/unit文件夹下.
  • 测试类主要包含一系列testAbc方法, 其中Abc通常是要被测试的类方法.
  • 测试方法通常包含一系列断言语句 (e.g. assertTrue, assertEquals),作为验证目标类行为的断点.

下面我们主要阐述如何为AR模型类编写单元测试. 我们的测试类将会继承自 CDbTestCase,因为它提供了数据库特定状态支持,在上一章节中我们已经详细讨论了数据库特定状态.

假设我们要测试blog案例中的Comment模型类,可以首先创建一个命名为CommentTest的类,然后将它保存为 protected/tests/unit/CommentTest.php:

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

    ......
}

在这个类中, 我们指定成员变量 fixtures 为一个包含这个测试要用到的特定状态(fixtures)数组。这个数组表示从特定状态名称到模型类的映射或者特定状态表名 (e.g. 从  posts 到 Post). 注意当映射到特定状态表名时,应该在数据表名称前加上冒号前缀 (e.g. ost)来区别于映射到模型类. 当使用模型类名称时,相应的表将会被看作特定状态表。正如我们之前所描述的 ,当一个测试方法执行后特定状态表每次将会被重置到某些已知的状态。

特定状态名称允许我们在测试方法中以一种很方便的方式访问特定状态数据. 下面的代码展示了典型的使用方法:

// 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: 如果一个特定状态声明使用它的数据表名 (e.g. 'posts'=>':Post'), 那么上述第三个使用方法将会无效,因为我们已经已经没有任何与模型类的关联信息了.

接下来,我们要编写testApprove方法在Comment模型类中测试approve方法. 代码非常简单明了: 首先我们插入一条待审核评论; 然后通过从数据库取出数据来验证这条带审核评论; 最后我们调用approve方法并且通过审核。

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'=>[email protected]',
        '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);
}