当需要测试的类依赖另外一个类的实例,而你又不想这两个类紧密关联时,Mock对象就派上用场了。Mock对象是原实例的“克隆体”,我们可以用它完成断言或替换原实例的某些功能来简化测试。
本文首先讲述如何在PHPUnit中创建和使用mock对象,然后再举例说明如何使用mock对象测试从数据库中取回数据的代码。
Mock 基础
在讲述PHPUnit如何实现mock对象之前,我们先看一下mock对象的工作原理。
创建一个mock对象非常简单,即使没有PHPUnit提供的那些精巧的方法,只需要创建一个类,继承你需要模拟的类即可:
class SomeClassMock extends SomeClass {
}
现在我们就可以使用这个mock对象,尽管它还没有什么实际用处。
假设待测的类会调用SomeClass的一个方法,现在我们需要确认该方法确实在测试中调用了。在mock类中增加一个变量和方法:
class SomeClassMock extends SomeClass {
public $methodWasCalled = false;
public function aMethod() {
//This method should get called by the object we are testing
$this->methodWasCalled = true;
}
}
现在,就可以在测试用例中这么做:
//Just a simple example
public function testAMethodIsCalled() {
//First, create a mock of SomeClass
$someClass = new SomeClassMock();
//This is the object we are testing, let's assume
//it takes a SomeClass instance as the parameter
$object = new MyClass($someClass);
//Say the method in SomeClass needs to be called now
$object->doSomething();
//Confirm the method was called by checking the mock's variable:
$this->assertTrue($someClass->methodWasCalled);
}
在PHPUnit中创建mock对象
像上面这么创建mock对象非常复杂和浪费时间,幸运地,我们并不需要自己写这些mock类,因为PHPUnit提供了一组方法,我们可以用它们完成大部分mock所需的工作。下面我们将使用这些API,不过了解背后的原理,总是有益处的。
我们使用PHPUnit的mock API重写一下上面的测试:
public function testAMethodIsCalled() {
//First, create a mock of SomeClass
$someClass = $this->getMock('SomeClass');
//Now we can tell the mock what we want to do:
$someClass->expects($this->once())
->method('aMethod');
//This is the object we are testing, let's assume
//it takes a SomeClass instance as the parameter
$object = new MyClass($someClass);
//Say the method in SomeClass needs to be called now
$object->doSomething();
//We don't need to do anything else - the mock object will confirm
//that the method was called for us.
}
简单多了吧?不用再为一个简单的方法调用检查去写一个完整的类,仅用getMock方法获取原对象的一个mock版本,然后告诉它这个测试中它需要做什么。
我们对这个mock对象做了什么呢?
第一步,调用expects方法,并将$this->once()作为参数传入。 第二步,以参数'aMethod‘调用method方法。上面操作意味着mock对象的aMethod方法将被调用一次。像这样用mock对象中的一个方法来替换原方法,即通常所说的桩方法——原对象中的方法被一个测试桩替代。
如果这个方法没有被调用,那会发生什么呢?运行测试的时候,将得到如下的一个类似错误:
1) testAMethodIsCalled(ExampleTest)
Expectation failed for method name is equal to <string:aMethod> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
使用mock对象测试数据库代码
在了解了mock对象之后,现在开始看一些真实的例子。
一个常见的情形是测试和数据库相关的代码,比如,待测代码会从数据库中取回或插入一些数据。这可以通过创建一个测试数据库来实现,但是用mock对象的话,事情将会更加简单。
测试样例
假设有一个EventRepository类,它记录应用中的某些事件。还有一个类Event,表示特定的事件。现在需要为EventRepository类编写单元测试,但是它需要访问数据库,使得问题变得很困难。
class EventRepository {
private $_pdo;
public function __construct(PDO $database) {
$this->_pdo = $database;
}
public function findById() {
$sql = 'SELECT * FROM events WHERE ...';
//Here we have some code to execute the SQL, convert
//the result to an Event object and then return it
}
/* some other methods here */
}
为了测试这个类,我们需要一个测试数据库,因为SQL查询直接在这个类内部执行,测试对此无能为力。
让EventRepository变得可测试
为了让这个类更合理,更易于测试,需要修改类,让它使用一个数据访问对象。这个数据访问对象(DAO)可以如下所示:
class EventDao {
private $_pdo;
public function __construct(PDO $pdo) {
$this->_pdo = $pdo;
}
public function findById() {
$sql = 'SELECT * FROM events WHERE ...';
//Instead of event repository, the code to execute SQL is here.
//We then return the row's data as an array
}
/* some other methods here */
}
然后,修改EventRepository类,使用EventDao:
class EventRepository {
private $_dao;
public function __construct(EventDao $dao) {
$this->_dao = $dao;
}
public function findById($id) {
$row = $this->_dao->findById($id);
//Now we simply process $row into an Event object. No direct DB access
}
/* some other methods here */
}
测试
现在可以测试这个类了!我们mock一个EventDao对象,这样测试代码就不需要访问数据库。
下面为findById写一个测试,示例如何创建DAO的mock对象:
public function testFindsCorrectEvent() {
//Set up a fake database row
$eventRow = array(
'id' => 1,
'name' => 'Awesome event'
);
$dao = $this->getMock('EventDao');
//Set up the mock to return the fake row when findById is called
$dao->expects($this->once())
->method('findById')
->with(1)
->will($this->returnValue($eventRow));
$repo = new EventRepository($dao);
$event = $repo->findById(1);
//Confirm ID of the returned Event is correct
$this->assertEquals(1, $event->getId());
}
这里包含了mock API的更多特性——检查参数和确定返回值。
首先,$eventRow 是mock对象dao的假返回值。因为仓库类要正常运行,就需要dao对象返回一个值,所以这里构造了一个返回值。
mock对象的expects 和method 方法和前面的一样,不过新加了with(1) 和will($this->returnValue($eventRow)) 的调用。
with 方法用于声明模拟方法需要哪些参数,这个例子要求传入参数事件ID:1。如果调用这个模拟方法时没有传入参数1,那么这个测试将会报告失败。
will 方法声明模拟方法被调用时它应该做什么,这里我们希望它返回我们构造的假返回值。
测试的其余部分创建我们测试的仓库实例,然后调用方法,最后断言返回值是正确的。这里没有提供findById的剩余代码,也没有提供Event的getId的实现,不过可以假设findById会用数据行创建Event实例,而Event拥有getId方法。
总结与进阶阅读
通过调用PHPUnit的mock API,可以非常方便地使用mock对象替换测试类对其他类的依赖,从而让测试变得简单。
我们没有列举全部的mock调用,比如如何抛出异常等。如果需要进一步研究,推荐阅读PHPUnit manual stubs and mocks chapter。
原文链接:http://codeutopia.net/blog/2009/06/26/unit-testing-4-mock-objects-and-testing-code-which-uses-the-database/
本文首先讲述如何在PHPUnit中创建和使用mock对象,然后再举例说明如何使用mock对象测试从数据库中取回数据的代码。
Mock 基础
在讲述PHPUnit如何实现mock对象之前,我们先看一下mock对象的工作原理。
创建一个mock对象非常简单,即使没有PHPUnit提供的那些精巧的方法,只需要创建一个类,继承你需要模拟的类即可:
class SomeClassMock extends SomeClass {
}
现在我们就可以使用这个mock对象,尽管它还没有什么实际用处。
假设待测的类会调用SomeClass的一个方法,现在我们需要确认该方法确实在测试中调用了。在mock类中增加一个变量和方法:
class SomeClassMock extends SomeClass {
public $methodWasCalled = false;
public function aMethod() {
//This method should get called by the object we are testing
$this->methodWasCalled = true;
}
}
现在,就可以在测试用例中这么做:
//Just a simple example
public function testAMethodIsCalled() {
//First, create a mock of SomeClass
$someClass = new SomeClassMock();
//This is the object we are testing, let's assume
//it takes a SomeClass instance as the parameter
$object = new MyClass($someClass);
//Say the method in SomeClass needs to be called now
$object->doSomething();
//Confirm the method was called by checking the mock's variable:
$this->assertTrue($someClass->methodWasCalled);
}
在PHPUnit中创建mock对象
像上面这么创建mock对象非常复杂和浪费时间,幸运地,我们并不需要自己写这些mock类,因为PHPUnit提供了一组方法,我们可以用它们完成大部分mock所需的工作。下面我们将使用这些API,不过了解背后的原理,总是有益处的。
我们使用PHPUnit的mock API重写一下上面的测试:
public function testAMethodIsCalled() {
//First, create a mock of SomeClass
$someClass = $this->getMock('SomeClass');
//Now we can tell the mock what we want to do:
$someClass->expects($this->once())
->method('aMethod');
//This is the object we are testing, let's assume
//it takes a SomeClass instance as the parameter
$object = new MyClass($someClass);
//Say the method in SomeClass needs to be called now
$object->doSomething();
//We don't need to do anything else - the mock object will confirm
//that the method was called for us.
}
简单多了吧?不用再为一个简单的方法调用检查去写一个完整的类,仅用getMock方法获取原对象的一个mock版本,然后告诉它这个测试中它需要做什么。
我们对这个mock对象做了什么呢?
第一步,调用expects方法,并将$this->once()作为参数传入。 第二步,以参数'aMethod‘调用method方法。上面操作意味着mock对象的aMethod方法将被调用一次。像这样用mock对象中的一个方法来替换原方法,即通常所说的桩方法——原对象中的方法被一个测试桩替代。
如果这个方法没有被调用,那会发生什么呢?运行测试的时候,将得到如下的一个类似错误:
1) testAMethodIsCalled(ExampleTest)
Expectation failed for method name is equal to <string:aMethod> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
使用mock对象测试数据库代码
在了解了mock对象之后,现在开始看一些真实的例子。
一个常见的情形是测试和数据库相关的代码,比如,待测代码会从数据库中取回或插入一些数据。这可以通过创建一个测试数据库来实现,但是用mock对象的话,事情将会更加简单。
测试样例
假设有一个EventRepository类,它记录应用中的某些事件。还有一个类Event,表示特定的事件。现在需要为EventRepository类编写单元测试,但是它需要访问数据库,使得问题变得很困难。
class EventRepository {
private $_pdo;
public function __construct(PDO $database) {
$this->_pdo = $database;
}
public function findById() {
$sql = 'SELECT * FROM events WHERE ...';
//Here we have some code to execute the SQL, convert
//the result to an Event object and then return it
}
/* some other methods here */
}
为了测试这个类,我们需要一个测试数据库,因为SQL查询直接在这个类内部执行,测试对此无能为力。
让EventRepository变得可测试
为了让这个类更合理,更易于测试,需要修改类,让它使用一个数据访问对象。这个数据访问对象(DAO)可以如下所示:
class EventDao {
private $_pdo;
public function __construct(PDO $pdo) {
$this->_pdo = $pdo;
}
public function findById() {
$sql = 'SELECT * FROM events WHERE ...';
//Instead of event repository, the code to execute SQL is here.
//We then return the row's data as an array
}
/* some other methods here */
}
然后,修改EventRepository类,使用EventDao:
class EventRepository {
private $_dao;
public function __construct(EventDao $dao) {
$this->_dao = $dao;
}
public function findById($id) {
$row = $this->_dao->findById($id);
//Now we simply process $row into an Event object. No direct DB access
}
/* some other methods here */
}
测试
现在可以测试这个类了!我们mock一个EventDao对象,这样测试代码就不需要访问数据库。
下面为findById写一个测试,示例如何创建DAO的mock对象:
public function testFindsCorrectEvent() {
//Set up a fake database row
$eventRow = array(
'id' => 1,
'name' => 'Awesome event'
);
$dao = $this->getMock('EventDao');
//Set up the mock to return the fake row when findById is called
$dao->expects($this->once())
->method('findById')
->with(1)
->will($this->returnValue($eventRow));
$repo = new EventRepository($dao);
$event = $repo->findById(1);
//Confirm ID of the returned Event is correct
$this->assertEquals(1, $event->getId());
}
这里包含了mock API的更多特性——检查参数和确定返回值。
首先,$eventRow 是mock对象dao的假返回值。因为仓库类要正常运行,就需要dao对象返回一个值,所以这里构造了一个返回值。
mock对象的expects 和method 方法和前面的一样,不过新加了with(1) 和will($this->returnValue($eventRow)) 的调用。
with 方法用于声明模拟方法需要哪些参数,这个例子要求传入参数事件ID:1。如果调用这个模拟方法时没有传入参数1,那么这个测试将会报告失败。
will 方法声明模拟方法被调用时它应该做什么,这里我们希望它返回我们构造的假返回值。
测试的其余部分创建我们测试的仓库实例,然后调用方法,最后断言返回值是正确的。这里没有提供findById的剩余代码,也没有提供Event的getId的实现,不过可以假设findById会用数据行创建Event实例,而Event拥有getId方法。
总结与进阶阅读
通过调用PHPUnit的mock API,可以非常方便地使用mock对象替换测试类对其他类的依赖,从而让测试变得简单。
我们没有列举全部的mock调用,比如如何抛出异常等。如果需要进一步研究,推荐阅读PHPUnit manual stubs and mocks chapter。
原文链接:http://codeutopia.net/blog/2009/06/26/unit-testing-4-mock-objects-and-testing-code-which-uses-the-database/
作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:https://jackxiang.com/post/3242/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!
评论列表