模拟对象单元测试的关键点
如果你是开发团队的一员,你的代码通常也依赖于队友编写的代码。但是,如果他们的代码目前不可用怎么办——例如,你的队友尚未完成编写?或者,如果你需要的代码需要其他难以设置的外部依赖项怎么办?如果由于你无法控制的其他因素而无法测试代码怎么办?你会只是闲逛,什么也不做,等待你的团队完成或一切就绪吗?当然不会!在本文中,我将说明如何编写解决此依赖项问题的代码。理想情况下,需要具备单元测试的一些背景知识,并且 SitePoint 上已经有 Michelle Saver 撰写的关于单元测试的优秀入门文章。虽然本文不需要,但请查看我关于自动化数据库测试的其他文章。
模拟对象的案例
你可能已经猜到,模拟对象可以解决我在引言中提到的棘手情况。但是什么是模拟对象?模拟对象是替代实际对象的实际实现的替代对象。为什么你想要一个替代对象而不是一个真实的对象?模拟对象用于单元测试,以在测试用例中模拟真实对象的运行行为。通过使用它们,你正在实现的对象的功能将更容易测试。以下是一些使用模拟对象有益的情况:
对象的一个或多个依赖项的实际实现尚未实现。假设你的任务是对数据库中的一些数据进行一些处理。你可能会调用某种形式的数据访问对象或数据存储库的方法,但是如果数据库尚未设置怎么办?如果没有任何数据可用(我遇到过太多次的情况)或者查询数据库的代码尚未编写怎么办?模拟数据访问对象通过返回一些预定义的值来模拟真实的数据访问对象。这使你免于设置数据库、查找数据或编写查询数据库的代码的负担。
对象的依赖项的实际实现取决于难以模拟的因素。假设你想按天统计 Facebook 帖子点赞和评论的数量。你从哪里获取数据?你的 Facebook 开发者帐户是新的。哎呀,你仍然没有朋友!你将如何模拟点赞和评论?模拟对象比麻烦你的同事为你点赞或评论一些帖子提供更好的方法。如果你想按天显示数据,你将如何模拟所有这些操作在一个时间范围内?按月呢?如果发生不可想象的事情,Facebook 目前宕机了怎么办?模拟对象可以假装是 Facebook 库并返回你所需的数据。你不必经历我刚才提到的麻烦就能开始执行你的任务。
模拟对象在实践中
既然我们知道什么是模拟对象,让我们看看一些实际示例。我们将实现前面提到的简单功能,例如统计 Facebook 帖子的点赞和评论。我们将从以下单元测试开始,以定义我们对对象将如何被调用以及返回值将是什么样的期望:
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
如果你运行此测试,你将得到一个失败的结果。这是预期的,因为我们还没有实现任何内容!现在让我们编写服务的实现。当然,第一步是从 Facebook 获取数据,所以让我们先这样做:
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
此测试也会失败,因为 Facebook 对象为空。你可以通过使用 Facebook 应用程序 ID 等创建实际实例来插入实际实现,但为了什么?我们都知道这样做会让你偏离手头的任务所带来的痛苦。我们可以通过注入模拟对象来避免这种情况!使用模拟对象(至少在我们的例子中)的方法是创建一个具有 get() 方法并返回模拟值的类。这应该会愚弄我们的客户端,使其认为它正在调用实际对象的实现,而实际上它只是被模拟了。
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { // test here } class MockFacebookLibrary { public function get($id) { return array( // mock return from Facebook here ); } }
现在我们有了模拟类,让我们实例化一个实例,然后将其注入 StatusService 以便可以使用它。但首先,使用 setter 更新 StatusService 以用于 Facebook 库:
<?php class StatusService { // other lines of code public function setFacebook($facebook) { $this->facebook = facebook; } }
现在注入模拟 Facebook 库:
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
测试仍然失败,但至少我们不再收到有关在非对象上调用方法的错误。更重要的是,你刚刚解决了满足此依赖项的需求。你现在可以开始编写你被分配的任务的业务逻辑并通过测试。
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
更进一步:使用模拟框架
虽然当你刚开始时可以使用手工制作的模拟对象,但后来,正如我自己发现的那样,随着你的需求变得越来越复杂,你可能需要使用一个真正的模拟框架。在本文中,我将展示如何使用 PHPUnit 附带的模拟框架。根据我的经验,与使用手动编写的模拟对象相比,使用模拟框架有一些好处:
使用 PHPUnit 的模拟框架
现在让我们关注使用 PHPUnit 的模拟框架,这些步骤实际上非常直观,一旦你掌握了它,它就会成为第二天性。在本节中,我们将使用 PHPUnit 的模拟框架为我们的示例情况创建一个模拟对象。但是,在我们这样做之前,请注释掉或删除测试中使用我们手工制作的模拟对象的代码行。我们需要先失败,这样我们才能通过。稍后,我们将注入新的模拟实现。
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { // test here } class MockFacebookLibrary { public function get($id) { return array( // mock return from Facebook here ); } }
验证运行 PHPUnit 时测试确实失败。现在,考虑一下我们如何手动模拟一个对象和我们想要调用的方法。我们做了什么?
第一步是识别要模拟的对象。在上面的分析示例功能中,我们模拟了 Facebook 库。我们正在执行与第一步相同的操作。
现在我们已经定义了要模拟的类,我们必须知道要模拟的类中的方法,如果存在任何方法,则指定参数和返回值。我在大多数情况下使用的基本模板大致如下:
让我们将刚才提到的步骤应用到我们的示例测试中。
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
在我们再次创建模拟 facebook 对象后,再次将其注入我们的服务中:
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
现在,你应该再次通过测试了。恭喜!你已经开始使用模拟对象进行测试了!希望你能够更有效地编程,最重要的是摆脱将来遇到的障碍依赖项。
图片来自 Fotolia
(此处应添加关于模拟对象测试的常见问题解答部分,内容与输入文本中的FAQ部分一致,但需进行轻微的改写和润色,以避免重复)
以上是PHP主|模拟对象测试的简介的详细内容。更多信息请关注PHP中文网其他相关文章!