PHPUnit袖珍指南 (9-12章)
时间:2007-02-05 来源:lib
第九章 测试优先编程
单元测试是几种软件开发实践和过程至关重要的部份,譬如测试优先编程,极限编程[3],测试驱动开发[4]。 单元测试也允许在结构上不支持的编程语言中支持契约式设计[5]。
[3] http://en.wikipedia.org/wiki/Extreme_Programming
[4] http://en.wikipedia.org/wiki/Test-driven_development
[5] http://en.wikipedia.org/wiki/Design_by_Contract
您可以完成代码后使用PHPUnit。但是,代码有错误,越快找到它们,测试就越有价值。因此,与其在代码错误“完成”后几个月才写测试,我们可以在缺陷引入后几天,几小时甚至几分钟内就写好它们。对了,为什么这样就够了?为什么不在一个缺陷引入前就写好测试呢?
测试优先编程,是极限编程和测试驱动开发的一部分,基于这种想法并发挥到了极限。以今天的计算机能力,我们可以每天进行数千次的测试,每个测试运行数千遍。我们能使用所有这些测试的反馈来编程,就向小步跑,除了以前有的测试之外,每步都有自动化的测试作保证。测试就是岩钉一样,保证不管发生了,一旦取得了进展,就会固定已有的进展。
当你第一次写测试时,它可能无法运行,因为你在调用还未实现的对象和方法。这也许起初让人感到奇怪,但你会很快习惯它。测试优先编程被认为是一种有效的的实践,符合面向对象的编程技术原则:面向接口而不是面向实现。当你写测试时,你就在考虑正在测试的对象的接口。从外面看,对象应该什么样?当你真正地让测试可以运行,你就是在考虑纯实现。接口会被失败的测试固定下来。
接下来简单介绍一些必要的测试优先编程内容。你可以在其他其它的书中找到更多的议题,譬如《举例说明测试驱动开发-Kent Beck》(Addison Wesley)或《测试驱动开发:Dave Astels的实践指南》(Prentice Hall)
第十章 代码覆盖率分析
你已经学会了怎么使用单元测试代码,但你怎么测试你的测试呢?你怎么发现没被测试的代码,换句话说,没被测试覆盖的代码?怎么衡量测试的完整性?所有这些问题的答案就是代码覆盖率分析。代码覆盖率分析告诉你当测试进行时,那些产品代码执行过了。
PHPUnit的代码覆盖率分析应用了Xdebug[6]扩展提供的语句覆盖率功能。 什么时语句覆盖率?举个例子来说,如果有一个方法有100个代码行,在测试进行时,只有75行真正运行了,这个方法的语句覆盖率就是75%。
[6] http://www.xdebug.org/
图1 显示了BankAccount(参见例12)的代码覆盖率报告。此HTML格式的报告是由PHPUnit命令行测试运行器生成的,使用--coverage-html选项。 黑字部分表示可执行的代码,灰字部分表示不可执行的代码,高亮代码行部分表示执行过的代码。
图1-1. BankAccount类没有被测试完全覆盖。
此代码覆盖率报告表示,我们要增加代码覆盖率的话,就需要增加setBalance(),depositMoney()和withdrawMoney()的测试,并使用合法的值。例14 显示了如何增加BankAccountTest类的测试用例来提高BankAccount类的代码覆盖率。
例14.用测试覆盖BankAccount类
require_once 'PHPUnit2/Framework/TestCase.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit2_Framework_TestCase {
// …
public function testSetBalance( ) {
$this->ba->setBalance(1);
$this->assertEquals(1, $this->ba->getBalance( ));
}
public function testDepositAndWidthdrawMoney( ) {
$this->ba->depositMoney(1);
$this->assertEquals(1, $this->ba->getBalance( ));
$this->ba->withdrawMoney(1);
$this->assertEquals(0, $this->ba->getBalance( ));
}
}
?>
见图2,我们看到类BankAccount已经被测试完全覆盖了。
图1-2
在本书后“PHPUnit 和Phing”一章中,你将学会怎么使用Phing生成更加详细的代码覆盖率报告。
第十一章残根
相比有多个错误来源的测试,只测试一件事情的测试提供了更好的信息。如何隔离测试的外部影响呢?用来自简单的PHP对象的残根替代昂贵, 杂乱, 不可靠, 缓慢, 复杂的资源。例如,为了单项测试的目的,你可以通过返回常数的方法来代表实际上很复杂的计算。
残根很好的解决了分配昂贵的外部资源的问题。例如,共享资源,在测试之间可以使用PHPUnit2 _ Extensions_TestSetup共享数据库连接,但是根本不使用为了测试目的的数据库更好。
改善设计是使用残根的一项效果。各种被使用的资源通过一个简单的面来访问,这样,用残根很容易实现资源替换。例如,不要在代码中到处进行数据库调用,而是实现一个简单的Idatabase接口。然后,就可以创建一个残根,实现Idatabase接口,用它来服务测试。甚而可以创建一个选项,选择使用数据库残根还是真正的数据库,这样测试既可以作为开发时的本地测试,也可以和真实的数据库进行集成测试。
需要形成残根的功能倾向于聚在一个对象中,以改进内聚性。用一个简单,内聚的接口来向外展示功能,你可以减少和系统其余部分的偶合性。
11-1.自分流
有时需要检查对象是否被正确地调用,你可以创建对象的完整残根用于调用,但那可能使它不便于检查调用的结果是否正确。一种更加简单的方案是使用自分流模式,把测试用例自身作为残根。术语自分流是从医学实践借用的术语,它指在动脉和静脉之间安装导管,从动脉引出血液并返回静脉,这可以注射药物。
Here is an example: suppose we want to test that the correct method is called on an object that observes another object. First, we make our test-case class an implementor of Observer:
以下是个例子:假设我们想要测试观察其它对象的对象是否被正确方法调用。首先,实现测试用例的Observer接口:
class ObserverTest extends PHPUnit2_Framework_TestCase
implements Observer{
}
其次, 我们实现Observer的方法update(),检查当它观察的对象的状态改变时,update()是否被调用:
public $wasCalled = FALSE;
public function update(Subject $subject) {
$this->wasCalled = TRUE;
}
现在,可以写测试了。创建一个新的Subject对象,将测试对象附带在对象上作为观察员。但Subject的状态改变时,如调用doSomething()方法,Subject对象就会调用所有注册为观察员的update()方法。通过实现update(),我们用$wasCalled变量来检查Subject是否做了它应该做的事。
public function testUpdate( ) {
$subject = new Subject;
$subject->attach($this);
$subject->doSomething( );
$this->assertTrue($this->wasCalled);
}
注意, 我们创建一个新的Subject的对象而不是依靠一个全局变量。残根模式鼓励这样的设计,它可以对象的偶合性,提高重用。
如果不熟悉自分流模式,测试可能难以阅读。这在做什么?为什么测试用例也是一个观察员?但是当你习惯了这些,测试就很容易阅读,所以你需要了解的测试都在一个类里。
第十二章 测试的其他用途
一旦你开始写自动测试,你就会想要发掘更多用途。以下是一些例子。
12-1. 敏捷文档
通常来说,在采用敏捷方法作为开发流程的项目中,如极限编程,文档很难和迅速变化的项目设计和代码同步。极限编程要求集体拥有代码,应此,每个开发人员都熟悉整个系统。如果你严格按照规定,编写的类具有自说明能力,你就可以用PHPUnit的TextDox功能根据测试来自动生成文档。这种文档给开发人员对每个类的用途描绘了一个概貌。
PHPUnit的TestDoc功能查找测试类的所有方法的名字,把它们转换成句子,如:testBalanceIsInitiallyZero( )变成“Balance is initially zero。”如果有几个测试方法名字的不同只在于前缀或数字,如testBalanceCannotBecomeNegative( ) 和 testBalanceCannotBecomeNegative2( ),转换过的句子“Balance cannot become negative”只会出现一次,其他这样的测试都会成功。
以下代码显示用PHPUnit 生成的关于银行帐号(Bank Account)类的敏捷文档,命令是:
phpunit --testdox-text BankAccountTest.txt BankAccountTest
结果显示:
BankAccount
- Balance is initially zero
- Balance cannot become negative
也可以用--testdox-html BankAccountTest.htm选项生成HTML格式的敏捷文档。
敏捷文档也用以用于为项目中的第三方包的假定条件来记录文档。当你使用一个外部包的时候,你也在冒一定的风险。外部包可能不想你预期的那样工作,或在未来版本中有微妙变化,导致你的代码不可运行。你也通过对外部包的假定条件编写测试来解决这个问题。如果你的测试成功了,你的假定条件还是有效。如果你对所有假定条件的测试都作了文档,那么,外部包的版本问题将不再是个问题:如果测试成功了,你的系统还会正常运行。
12-2.跨团队测试
当你将测试的假定条件文档化后,你就要为你的测试负责。你和包的提供者一起确定这些假设,你要记住,包的提供者对你的测试一无所知。如果你想要与包的提供者形成更密切的关系,你可以使用测试来沟通协调相关活动。
当你同意和包的供应商协调活动时, 你们可以一起写测试。 这样做,测试会显露出尽可能许多的假定。隐藏假定条件会导致合作失败。通过测试,你可以在提供的包中地将你的期望准确记入文档。当所有的测试运行完毕时,包的供应商就知道它正常运行了。
使用残根(参见本书前面的“残根”一章),你能进一步减弱和包供应商之间的耦合。供应商的工作是测试能够运行在包的真正实现上。你的工作是测试运行在自己的代码上。在你得到供应商的包的真正的实现之前,你使用残根工作。用这种方法,两个团队能够独立工作。
12-3 调试测试
当你得到一份缺陷报告时,你的第一冲动也许是赶快修复它。经验表明,这种冲动效果不好:修复此缺陷可能会导致其它缺陷。
你应该通过检查如下列表来控制冲动:
- 再次核实一下,确信你能重现缺陷。
- 发现最小范围的出现缺陷的代码。例如,输出时如果数字不正确,找到计算那个数字的对象。
- 写一个自动化的测试,如果该缺陷被修复,此测试会通过,反之会失败。
- 修复缺陷。
发现缺陷的最小可靠再现部分提供了一个发现缺陷真正原因的机会。你写的测试将会提高正确修复缺陷的机会,因为新的测试减少了当未来代码改变时取消本次修复的可能性。
前面所写的所有测试都会减少因疏忽而造成其它问题的可能性。
12-4 重构
重构是一种改进现有的代码的设计的受控技术,只有当你有一套测试套件时才能被安全的应用。否则,你也许没有注意到当你修改结构时系统崩溃了。 重构可以分解为一系列行为保留的小的改进,这种改进不会改变程序的行为。
以下情况将帮助你改进项目的代码和设计,使用单位测试验证重构的变革步奏是行为保留的,不会引入其它错误:
- 所有单位测试运行正确。
- 代码传达它的设计意图。
- 代码没有冗余。
- 代码只包含最小数量的类和方法。
单元测试是几种软件开发实践和过程至关重要的部份,譬如测试优先编程,极限编程[3],测试驱动开发[4]。 单元测试也允许在结构上不支持的编程语言中支持契约式设计[5]。
[3] http://en.wikipedia.org/wiki/Extreme_Programming
[4] http://en.wikipedia.org/wiki/Test-driven_development
[5] http://en.wikipedia.org/wiki/Design_by_Contract
您可以完成代码后使用PHPUnit。但是,代码有错误,越快找到它们,测试就越有价值。因此,与其在代码错误“完成”后几个月才写测试,我们可以在缺陷引入后几天,几小时甚至几分钟内就写好它们。对了,为什么这样就够了?为什么不在一个缺陷引入前就写好测试呢?
测试优先编程,是极限编程和测试驱动开发的一部分,基于这种想法并发挥到了极限。以今天的计算机能力,我们可以每天进行数千次的测试,每个测试运行数千遍。我们能使用所有这些测试的反馈来编程,就向小步跑,除了以前有的测试之外,每步都有自动化的测试作保证。测试就是岩钉一样,保证不管发生了,一旦取得了进展,就会固定已有的进展。
当你第一次写测试时,它可能无法运行,因为你在调用还未实现的对象和方法。这也许起初让人感到奇怪,但你会很快习惯它。测试优先编程被认为是一种有效的的实践,符合面向对象的编程技术原则:面向接口而不是面向实现。当你写测试时,你就在考虑正在测试的对象的接口。从外面看,对象应该什么样?当你真正地让测试可以运行,你就是在考虑纯实现。接口会被失败的测试固定下来。
接下来简单介绍一些必要的测试优先编程内容。你可以在其他其它的书中找到更多的议题,譬如《举例说明测试驱动开发-Kent Beck》(Addison Wesley)或《测试驱动开发:Dave Astels的实践指南》(Prentice Hall)
第十章 代码覆盖率分析
你已经学会了怎么使用单元测试代码,但你怎么测试你的测试呢?你怎么发现没被测试的代码,换句话说,没被测试覆盖的代码?怎么衡量测试的完整性?所有这些问题的答案就是代码覆盖率分析。代码覆盖率分析告诉你当测试进行时,那些产品代码执行过了。
PHPUnit的代码覆盖率分析应用了Xdebug[6]扩展提供的语句覆盖率功能。 什么时语句覆盖率?举个例子来说,如果有一个方法有100个代码行,在测试进行时,只有75行真正运行了,这个方法的语句覆盖率就是75%。
[6] http://www.xdebug.org/
图1 显示了BankAccount(参见例12)的代码覆盖率报告。此HTML格式的报告是由PHPUnit命令行测试运行器生成的,使用--coverage-html选项。 黑字部分表示可执行的代码,灰字部分表示不可执行的代码,高亮代码行部分表示执行过的代码。
图1-1. BankAccount类没有被测试完全覆盖。
此代码覆盖率报告表示,我们要增加代码覆盖率的话,就需要增加setBalance(),depositMoney()和withdrawMoney()的测试,并使用合法的值。例14 显示了如何增加BankAccountTest类的测试用例来提高BankAccount类的代码覆盖率。
例14.用测试覆盖BankAccount类
require_once 'PHPUnit2/Framework/TestCase.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit2_Framework_TestCase {
// …
public function testSetBalance( ) {
$this->ba->setBalance(1);
$this->assertEquals(1, $this->ba->getBalance( ));
}
public function testDepositAndWidthdrawMoney( ) {
$this->ba->depositMoney(1);
$this->assertEquals(1, $this->ba->getBalance( ));
$this->ba->withdrawMoney(1);
$this->assertEquals(0, $this->ba->getBalance( ));
}
}
?>
见图2,我们看到类BankAccount已经被测试完全覆盖了。
图1-2
在本书后“PHPUnit 和Phing”一章中,你将学会怎么使用Phing生成更加详细的代码覆盖率报告。
第十一章残根
相比有多个错误来源的测试,只测试一件事情的测试提供了更好的信息。如何隔离测试的外部影响呢?用来自简单的PHP对象的残根替代昂贵, 杂乱, 不可靠, 缓慢, 复杂的资源。例如,为了单项测试的目的,你可以通过返回常数的方法来代表实际上很复杂的计算。
残根很好的解决了分配昂贵的外部资源的问题。例如,共享资源,在测试之间可以使用PHPUnit2 _ Extensions_TestSetup共享数据库连接,但是根本不使用为了测试目的的数据库更好。
改善设计是使用残根的一项效果。各种被使用的资源通过一个简单的面来访问,这样,用残根很容易实现资源替换。例如,不要在代码中到处进行数据库调用,而是实现一个简单的Idatabase接口。然后,就可以创建一个残根,实现Idatabase接口,用它来服务测试。甚而可以创建一个选项,选择使用数据库残根还是真正的数据库,这样测试既可以作为开发时的本地测试,也可以和真实的数据库进行集成测试。
需要形成残根的功能倾向于聚在一个对象中,以改进内聚性。用一个简单,内聚的接口来向外展示功能,你可以减少和系统其余部分的偶合性。
11-1.自分流
有时需要检查对象是否被正确地调用,你可以创建对象的完整残根用于调用,但那可能使它不便于检查调用的结果是否正确。一种更加简单的方案是使用自分流模式,把测试用例自身作为残根。术语自分流是从医学实践借用的术语,它指在动脉和静脉之间安装导管,从动脉引出血液并返回静脉,这可以注射药物。
Here is an example: suppose we want to test that the correct method is called on an object that observes another object. First, we make our test-case class an implementor of Observer:
以下是个例子:假设我们想要测试观察其它对象的对象是否被正确方法调用。首先,实现测试用例的Observer接口:
class ObserverTest extends PHPUnit2_Framework_TestCase
implements Observer{
}
其次, 我们实现Observer的方法update(),检查当它观察的对象的状态改变时,update()是否被调用:
public $wasCalled = FALSE;
public function update(Subject $subject) {
$this->wasCalled = TRUE;
}
现在,可以写测试了。创建一个新的Subject对象,将测试对象附带在对象上作为观察员。但Subject的状态改变时,如调用doSomething()方法,Subject对象就会调用所有注册为观察员的update()方法。通过实现update(),我们用$wasCalled变量来检查Subject是否做了它应该做的事。
public function testUpdate( ) {
$subject = new Subject;
$subject->attach($this);
$subject->doSomething( );
$this->assertTrue($this->wasCalled);
}
注意, 我们创建一个新的Subject的对象而不是依靠一个全局变量。残根模式鼓励这样的设计,它可以对象的偶合性,提高重用。
如果不熟悉自分流模式,测试可能难以阅读。这在做什么?为什么测试用例也是一个观察员?但是当你习惯了这些,测试就很容易阅读,所以你需要了解的测试都在一个类里。
第十二章 测试的其他用途
一旦你开始写自动测试,你就会想要发掘更多用途。以下是一些例子。
12-1. 敏捷文档
通常来说,在采用敏捷方法作为开发流程的项目中,如极限编程,文档很难和迅速变化的项目设计和代码同步。极限编程要求集体拥有代码,应此,每个开发人员都熟悉整个系统。如果你严格按照规定,编写的类具有自说明能力,你就可以用PHPUnit的TextDox功能根据测试来自动生成文档。这种文档给开发人员对每个类的用途描绘了一个概貌。
PHPUnit的TestDoc功能查找测试类的所有方法的名字,把它们转换成句子,如:testBalanceIsInitiallyZero( )变成“Balance is initially zero。”如果有几个测试方法名字的不同只在于前缀或数字,如testBalanceCannotBecomeNegative( ) 和 testBalanceCannotBecomeNegative2( ),转换过的句子“Balance cannot become negative”只会出现一次,其他这样的测试都会成功。
以下代码显示用PHPUnit 生成的关于银行帐号(Bank Account)类的敏捷文档,命令是:
phpunit --testdox-text BankAccountTest.txt BankAccountTest
结果显示:
BankAccount
- Balance is initially zero
- Balance cannot become negative
也可以用--testdox-html BankAccountTest.htm选项生成HTML格式的敏捷文档。
敏捷文档也用以用于为项目中的第三方包的假定条件来记录文档。当你使用一个外部包的时候,你也在冒一定的风险。外部包可能不想你预期的那样工作,或在未来版本中有微妙变化,导致你的代码不可运行。你也通过对外部包的假定条件编写测试来解决这个问题。如果你的测试成功了,你的假定条件还是有效。如果你对所有假定条件的测试都作了文档,那么,外部包的版本问题将不再是个问题:如果测试成功了,你的系统还会正常运行。
12-2.跨团队测试
当你将测试的假定条件文档化后,你就要为你的测试负责。你和包的提供者一起确定这些假设,你要记住,包的提供者对你的测试一无所知。如果你想要与包的提供者形成更密切的关系,你可以使用测试来沟通协调相关活动。
当你同意和包的供应商协调活动时, 你们可以一起写测试。 这样做,测试会显露出尽可能许多的假定。隐藏假定条件会导致合作失败。通过测试,你可以在提供的包中地将你的期望准确记入文档。当所有的测试运行完毕时,包的供应商就知道它正常运行了。
使用残根(参见本书前面的“残根”一章),你能进一步减弱和包供应商之间的耦合。供应商的工作是测试能够运行在包的真正实现上。你的工作是测试运行在自己的代码上。在你得到供应商的包的真正的实现之前,你使用残根工作。用这种方法,两个团队能够独立工作。
12-3 调试测试
当你得到一份缺陷报告时,你的第一冲动也许是赶快修复它。经验表明,这种冲动效果不好:修复此缺陷可能会导致其它缺陷。
你应该通过检查如下列表来控制冲动:
- 再次核实一下,确信你能重现缺陷。
- 发现最小范围的出现缺陷的代码。例如,输出时如果数字不正确,找到计算那个数字的对象。
- 写一个自动化的测试,如果该缺陷被修复,此测试会通过,反之会失败。
- 修复缺陷。
发现缺陷的最小可靠再现部分提供了一个发现缺陷真正原因的机会。你写的测试将会提高正确修复缺陷的机会,因为新的测试减少了当未来代码改变时取消本次修复的可能性。
前面所写的所有测试都会减少因疏忽而造成其它问题的可能性。
12-4 重构
重构是一种改进现有的代码的设计的受控技术,只有当你有一套测试套件时才能被安全的应用。否则,你也许没有注意到当你修改结构时系统崩溃了。 重构可以分解为一系列行为保留的小的改进,这种改进不会改变程序的行为。
以下情况将帮助你改进项目的代码和设计,使用单位测试验证重构的变革步奏是行为保留的,不会引入其它错误:
- 所有单位测试运行正确。
- 代码传达它的设计意图。
- 代码没有冗余。
- 代码只包含最小数量的类和方法。
相关阅读 更多 +
排行榜 更多 +