zoukankan      html  css  js  c++  java
  • PHPUnit学习03使用Mock对象解决测试依赖

    本文目的

    单元测试过程中经常会遇到被测试函数A依赖另一个函数B,但是B已经完全测试过,没有必要在测试A的时候重复测试B。如何去除这种不必要的测试呢?本文探讨了如何手动解决测试依赖,更进一步地,结合PHPUnit的Mock API,提出更加优雅,高效的解决方案。

    一个例子

    假设有一个订单管理类OrderManager,它的私有变量中,有一个OrderDao,当插入订单时,首先OrderManager会检查内参数是否合法,然后调用OrderDao的insert方法,将Order对象插入到数据库中。现在,假设已经测底的对OrderDao的所有方法进行了单元测试,需要测试OrderManager相关方。此时,就产生了测试依赖的问题。具体代码如下:

    <?php
    class OrderDao{
    	public function insert($aOrder){	
    		if($aOrder['id'] == 'order_id_already_existing'){
    			return -1;
    		}
    		
    		// 这个方法只是简单的模拟,不作实质的数据库操作
    		print "connect to db\n";
    		print "execute query\n";
    		print "1 row effected\n";
    		print "insert order {$aOrder['id']} successfully\n";
    		return 0;
    	}
    }
    
    class OrderManager{
    	private $_oOrderDao;
    	public function __construct(OrderDao $oOd){
    		$this->_oOrderDao = $oOd;
    	}
    	public function insertOrder($aOrder){
    		if(array_key_exists('id', $aOrder) && $aOrder['id'] != ''){
    			print "order {$aOrder['id']} is valide!\n";
    			if($this->_oOrderDao->insert($aOrder) == 0){
    				print "call dao insert successfully\n";
    				return true;
    			}
    			else{
    				print "insert error!\n";
    				return false;
    			}			
    		}else{
    			print "order {$aOrder['id']} is invalide!\n";
    			return false;
    		}
    	}
    }
    
    ?>

    假设上面的文件中,OrderDao已经被测试测试,现在需要测试OrderManager::insertOrder方法。这个方法调用了OrderDao::insert方法。下面,先看看手动创建一个mock(mock的中文意识是“模仿”)类进行单元测试的方案。

    手动创建Mock

    创建一个新的类,称为OrderDaoMock,继承类OrderDao,方法实现时采用一些简单,方便,无意义的实现,如下(ut_order_demo_manual.php):

    <?php
    require_once 'order_demo.php';
    
    class OrderDaoMock extends OrderDao{
    	public function insert($aOrder){
    		print "Mock Info: insert order {$aOrder['id']} successfully\n";
    		return 0;
    	}
    }
    
    class OrderDemo_TestCase extends PHPUnit_Framework_TestCase{
    	public function testNullIdOrder(){
    		$oOm = new OrderManager(new OrderDaoMock());
    		$aOrder = array('id'=>'');
    		$this->assertFalse($oOm->insertOrder($aOrder));
    	}
    	
    	public function testNoIdOrder(){
    		$oOm = new OrderManager(new OrderDaoMock());
    		$aOrder = array();
    		$this->assertFalse($oOm->insertOrder($aOrder));
    	}
    	
    	public function testSuccessInsertOrder(){
    		$oOm = new OrderManager(new OrderDaoMock());
    		$aOrder = array('id'=>'bourneli123456789');
    		$this->assertTrue($oOm->insertOrder($aOrder));
    	}
    }
    
    ?>

    执行结果:

    clip_image002

    上面的方法看似不复杂,只是继承了OrderDao类,实现了一个哑方法insert。但是,设想如果在真实的系统中,被测试的方法会涉及到许多其他对象,如果每个对象都手动创建一个mock类,工作量还是十分大的。还好,PHPUnit提供了Mock类的API,可以方便的创建这些mock类。

    PHPUnit的Mock API

    现在,我们看看使用PHPUint的Mock API的版本(ut_order_demo_mock.php):

    <?php
    require_once 'order_demo.php';
    
    class OrderDemo_TestCase extends PHPUnit_Framework_TestCase
    {
    	public function testNullIdOrder(){
            	//自动创建一个集成OrderDao的mock对象
    		$oMockOrderDao = $this->getMock('OrderDao'); 
    		//期望不要调用这个对象的insert方法,如果调用,就会报错
            	$oMockOrderDao->expects($this->never())->method('insert'); 
    		
    		$oOm = new OrderManager($oMockOrderDao);
    		$aOrder = array('id'=>'');
    		$this->assertFalse($oOm->insertOrder($aOrder));
    	}
    	
    	public function testNoIdOrder(){
    		$oMockOrderDao = $this->getMock('OrderDao');
    		$oMockOrderDao->expects($this->never())->method('insert');
    	
    		$oOm = new OrderManager($oMockOrderDao);
    
    		$aOrder = array();
    		$this->assertFalse($oOm->insertOrder($aOrder));
    	}
    	
    	public function testSuccessInsertOrder(){
    		$aOrder = array('id'=>'bourneli123456789');
    		
    		$oMockOrderDao = $this->getMock('OrderDao');
    		$oMockOrderDao->expects($this->once())
    					  ->method('insert');
    	
    		$oOm = new OrderManager($oMockOrderDao);
    		$this->assertTrue($oOm->insertOrder($aOrder));
    	}
    	
    	public function testOrderIdExisting(){
    		$aOrder = array('id'=>'order_id_already_existing dfd');
    		//自动创建一个集成OrderDao的mock对象
    		$oMockOrderDao = $this->getMock('OrderDao'); 
    		//期望调用这个对象insert方法,次数任意。在调用时,输入必须是$aOrder对象,
    		//返回必须是0。如果不满足这种期望,将会报错。
    		$oMockOrderDao->expects($this->any())
    					  ->method('insert')
    					  ->with($aOrder)
    					  ->will($this->returnValue(0));
    	
    		$oOm = new OrderManager($oMockOrderDao);
    		$this->assertTrue($oOm->insertOrder($aOrder));//真实的调用,并断言调用结果
    	}
    }
    
    ?>

    上面的例子的执行结果如下:

    clip_image004

    我们来分析一下上面的代码,

    $oMockOrderDao = $this->getMock('OrderDao');

    上面这段代码就为我们完成了手动创建OrderDaoMock类的工作。

    $oMockOrderDao->expects($this->any())->method('insert')
    		->with($aOrder)->will($this->returnValue(0));

    上面这段代码代码为我们完成了四个针对mock对象调用的断言:

    1)调用insert方法;

    2)调用任意次;

    3)调用时,输入参数必须是$aOrder对象;

    4)调用结束后,返回参数必须是0.

    Mock API给了我们很大的自由度,可以随意操作mock对象的行为,使得用mock进行单元测试十分便捷。根据上面的例子亲自动手实践,你会很容易理解mock对象的原理和作用。

    Mock对象

    为什么需要mock对象呢?有时候,很难测试被测系统(System Under Test,“被测系统”以下简称SUT),因为SUT依赖一些不能在测试环境使用的组件。这些组件有可能不可用(如第三方系统),或者他们不能返回测试中期望的结果,或者是这些组件执行后会带来负面效果(如修改数据库中的数据)。这时候,就需要mock对象来解决这些问题。Mock对象提供相同的API,供SUT调用,使得SUT可以正常运转。如果希望在测试中大范围的使用mock对象,对程序开发而言也有要求,程序开发过程中必须依照高内聚,底耦合的策略,并且尽量使用接口编程,这样mock类才可以去模仿——通过继承和多态,否则mock对象没有用武之地。这一点,也暴露出了mock类的短板,mock只能模拟类中的public,protected函数,如果是static, private或final函数,mock对象无能为力。

    相关链接

  • 相关阅读:
    Flask + vue 前后端分离的 二手书App
    Kafka 0.10.0.1 consumer get earliest partition offset from Kafka broker cluster
    Kafka topic Schema version mismatch error
    ORM的多表查询详述
    ORM多表操作之创建关联表及添加表记录
    ORM的单表操作
    Django的模板层简介
    Django的视图层简介
    Django中的路由配置简介
    Django简介及Django项目的创建详述
  • 原文地址:https://www.cnblogs.com/bourneli/p/2570440.html
Copyright © 2011-2022 走看看