测试 是软件开发中一个必不可少的环节。不管我们是否有意识到,其实我们经常都在做测试工作。比如最原始的在代码中插入 echo $variable 查看变量值,简单粗暴而有效。当然,PHP 也会有 XDebug 这样的工具,可以做到单步调试,不过要在服务器上安装再配合好本地一个 IDE 和浏览器一起运作,始终不是那么方便。

我们实现了一些复杂逻辑后,总会要输入一些数据,来确认程序是否能给出预期的结果,页面是否正常展示。更高级的开发者会写一些代码来自动完成这个测试过程,这样一来每当我们需要测试一些东西的时候,只需要调用测试脚本,就能直接获知结果了。这就是所谓的 自动测试

软件测试已经发展为一个独立的工种,黑盒、白盒、单元测试、功能测试……流程都比较规范化了。随着软件开发过程的不断演变,各种方法论层出不穷,类似的名词包括:敏捷开发、持续集成、XP极限编程、结对编程、TDD……一般谈这些概念的时候都会被大牛黑出翔来,比如酷壳博主 @左耳朵耗子 发表过这样一条微博:

不会写程序的人来搞什么软件开发咨询,SQA,流程设计,软件项目管理,全是扯蛋。所以,程序员应该要像 Linus 一样自信的对这些人说:“Talk is cheap, show me the code.”

习总书记教育我们:

空谈误国。

我想聊一下 TDD。我必须声明我并非想借技术名词炒作概念。自从上次一篇 关于MVC的讨论 被新浪 SAE 的微博大号转发后,引来口水无数,各种被喷。其实我也只是初学者,谈谈自己对编程的理解,希望能尝试一下不同的实践方式而已,最终想法和所有开发者一样,都是要提高代码质量和生产效率。

TDD 简介

TDD 测试驱动开发,这个概念来自于敏捷中的极限编程(XP),其中的主要观点即:测试对项目成败起决定作用。测试应该自动化,经常测,并尽可能在功能代码还没实现之前就写好测试代码。在 XP 理论中,架构设计应该是恒定的不可随意改动的,而代码实现却会不断迭代和重构。

XP-feedback

所谓的 测试驱动开发 的开发周期:

  1. 创建一个涵盖要实现的特性的新的测试。测试预计将在第一次执行的时候失败,因为特性尚未实现。
  2. 执行所有测试,确保这个新的测试是失败的。
  3. 编写代码来使得测试通过。
  4. 执行所有测试,确保所有测试通过。
  5. 重构新编写的代码并确保这些测试仍然能够通过。

重复步骤1至5推进整体功能的实现。

PHP 测试工具与实践

我所使用的 Yii Framework 支持单元测试和功能测试,这里会用到 PHPUnitSelenium Remote Control 这两个测试工具。

PHPUnit 很多人都在用,通过很多断言语句(比如 assertTrue, assertEquals)来检查验证目标代码的行为。例如下面这个例子,用于测试博客评论功能。

// CDbTestCase 由 Yii 框架继承自 PHPUnit 中的 PHPUnit_Framework_TestCase
class CommentTest extends CDbTestCase 
{
    public function testApprove()
    {
        // 插入一条新的评论
        $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));
 
        // 验证评论处于 pending 状态
        $comment=Comment::model()->findByPk($comment->id);
        $this->assertTrue($comment instanceof Comment);
        $this->assertEquals(Comment::STATUS_PENDING,$comment->status);
 
        // 调用 approve() 方法后,再验证评论状态为“已通过”
        $comment->approve();
        $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
        $comment=Comment::model()->findByPk($comment->id);
        $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
    }
 
    ......
}

Selenium 这个工具的介绍看上去感觉很强大,可以通过模拟浏览器访问 Web 页面的方式,来进行 UI 测试。用法和上面类似,

// WebTestCase 由 Yii 框架继承自 PHPUnit_Extensions_SeleniumTestCase
class PostTest extends WebTestCase
{ 
    public function testShow()
    {
        $this->open('post/1');
        // 验证测试页面文章标题
        $this->assertTextPresent($this->posts['sample1']['title']);
        // 验证评论表单
        $this->assertTextPresent('Leave a Comment');
    }
 
    ......
}

我的观点

TDD 并非教条。敏捷专家或是各种咨询师在宣扬敏捷,很多团队却被整得很苦逼,就是因为他们像宗教一样地信奉教条而不知变通。比如,TDD 就等同于 Unit Test 吗?我们真的一定要在写代码前就写好测试吗?这些问题都是值得商榷的,也是争论很多的。陈皓曾经谈到 TDD 的几个弊端——

  • 1)TDD 可能会让程序员敷衍了事,以为 test case 没有错就正确了。
  • 2)TDD 可能会让你忽略了软件设计和架构以及程序的扩展性和重用性。

令人沮丧的是,现在说这些有点纸上谈兵,因为我看到很多 PHP 程序员压根就没有写测试,自己手动查看页面没问题后就上线了,然后陷入无尽的 bug report 循环。这属于 各种流行的编程风格 中的撞大运编程。我觉得程序员编写单元测试还是很有必要的,虽然会增加工作量,但以后的代码维护会轻松很多。在 StackOverflow 上有人问 单元测试要测到什么程度,“老板为我的代码付报酬,而不是测试”。最值得借鉴的答案则强调并非面面俱到,只是特别注意边界情况,那些会让团队容易出错的代码。

众所周知,Facebook 没有专职的测试工程师,这是他们在质量控制中引以为豪并备受瞩目的一点。所有的测试都是程序员自己完成,对自己的代码质量负责,也就是 Eat your own dog food。每个程序员都是多面手,从最开始的一个 idea,到自己分析需求、架构,开发原型,自我管理版本控制,自己去测自己写的代码,自己去运维去部署。这样才会对整个开发的过程有深刻的认识,亲身体会某个环节的痛苦才会有想要改进的动力。

总结

每一个 PHP 程序员上辈子都是折翼的前端、美工、系统管理员和测试工程师……