如何在PHP单元测试中模拟构造函数中调用的方法?
我在单元测试一个在构造函数中调用了方法的类时遇到了问题。我不明白怎么嘲笑这个。也许我应该使用phpUnit的“设置”方法 我在用模拟图书馆。有比这更好的工具吗如何在PHP单元测试中模拟构造函数中调用的方法?,php,unit-testing,mocking,phpunit,mockery,Php,Unit Testing,Mocking,Phpunit,Mockery,我在单元测试一个在构造函数中调用了方法的类时遇到了问题。我不明白怎么嘲笑这个。也许我应该使用phpUnit的“设置”方法 我在用模拟图书馆。有比这更好的工具吗 如果您有任何建议,我们将不胜感激。只要扩展这个类并重写您的方法(如果它是公共的或受保护的)。这里的问题是,该方法无法模拟,因为对象尚未实例化。 sectus的答案是有效的,但可能不是很灵活,因为在不同的测试中很难改变模拟方法的行为 您可以创建另一个与要模拟的方法相同的类,并将该类的实例作为构造函数参数传递。这样,您就可以通过模拟类的测试
如果您有任何建议,我们将不胜感激。只要扩展这个类并重写您的方法(如果它是公共的或受保护的)。这里的问题是,该方法无法模拟,因为对象尚未实例化。 sectus的答案是有效的,但可能不是很灵活,因为在不同的测试中很难改变模拟方法的行为
您可以创建另一个与要模拟的方法相同的类,并将该类的实例作为构造函数参数传递。这样,您就可以通过模拟类的测试。通常,您遇到的问题是一个类做了太多事情的气味。要测试这个类,您需要模拟内部对象(methodToMock),然后使用依赖项注入来传递模拟的服务,而不是真正的服务 类别:
class ToTest{
private $svc;
// Constructor Injection, pass the Service object here
public function __construct($Service = NULL)
{
if(! is_null($Service) )
{
if($Service instanceof YourService)
{
$this->SetService($Service);
}
}
}
function SetService(YourService $Service)
{
$this->svc = $Service
}
function DoSomething($request) {
$svc = $this->svc;
$result = $svc->getResult($request); // Get Result from Real Service
return $result;
}
function DoSomethingElse($Input) {
// do stuff
return $Input;
}
}
测试:
如果您的类很难实例化以进行测试,那么这是一种代码气味,表明您的类在构造函数中做了太多或太多的工作 警告标志
- 构造函数或at字段声明中的新关键字
- 在构造函数或at字段声明中调用静态方法
- 构造函数中的字段分配以外的任何内容
- 构造函数完成后对象未完全初始化(请注意 输出(用于初始化方法)
- 构造函数中的控制流(条件逻辑或循环逻辑)
- 代码在构造函数中进行复杂的对象图构造 而不是使用工厂或建筑商
- 添加或使用初始化块
methodToMock
函数在构造函数中做什么,都需要重新思考。正如其他答案中提到的,您可能希望使用依赖项注入来传递类正在做的事情
重新思考您的类实际在做什么,并进行重构,以便更易于测试。这还有一个好处,就是使您的类更容易在以后重用和修改。我也想知道这就是我发现您的问题的原因。最后我决定做一些有点脏的事。。。使用反射 下面是我要测试的方法:
/**
* ArrayPool constructor.
* @param array $tasks Things that might be tasks
*/
public function __construct(array $tasks)
{
foreach ($tasks as $name => $parameters) {
if ($parameters instanceof TaskInterface) {
$this->addTask($parameters);
continue;
}
if ($parameters instanceof DescriptionInterface) {
$this->addTask(new Task($parameters));
continue;
}
$this->addPotentialTask($name, $parameters);
}
}
就本测试而言,我不想实际运行->addTask
或->addPotentialTask
,只知道会调用它们
以下是测试:
/**
* @test
* @covers ::__construct
* @uses \Foundry\Masonry\Core\Task::__construct
*/
public function testConstruct()
{
$task = $this->getMockForAbstractClass(TaskInterface::class);
$description = $this->getMockForAbstractClass(DescriptionInterface::class);
$namedTask = 'someTask';
$parameters = [];
$arrayPool =
$this
->getMockBuilder(ArrayPool::class)
->disableOriginalConstructor()
->setMethods(['addTask', 'addPotentialTask'])
->getMock();
$arrayPool
->expects($this->at(0))
->method('addTask')
->with($task);
$arrayPool
->expects($this->at(1))
->method('addTask')
->with($this->isInstanceOf(TaskInterface::class));
$arrayPool
->expects($this->at(2))
->method('addPotentialTask')
->with($namedTask, $parameters);
$construct = $this->getObjectMethod($arrayPool, '__construct');
$construct([
0=>$task,
1=>$description,
$namedTask => $parameters
]);
}
魔法发生在getObjectMethod
中,它接受一个对象并返回一个可调用的闭包,该闭包将在该对象的实例上调用该方法:
/**
* Gets returns a proxy for any method of an object, regardless of scope
* @param object $object Any object
* @param string $methodName The name of the method you want to proxy
* @return \Closure
*/
protected function getObjectMethod($object, $methodName)
{
if (!is_object($object)) {
throw new \InvalidArgumentException('Can not get method of non object');
}
$reflectionMethod = new \ReflectionMethod($object, $methodName);
$reflectionMethod->setAccessible(true);
return function () use ($object, $reflectionMethod) {
return $reflectionMethod->invokeArgs($object, func_get_args());
};
}
我知道循环和条件都能正确运行,而不会进入我不想在这里输入的代码
TL;医生:
\u构造
\uuu construct
谢谢你的回答!非常感谢,你能举个简单的例子吗?谢谢:)解释得很清楚!谢谢我会试试的,我会告诉你是怎么回事!然后被接受的答案将是:)没问题,希望它能帮助你。
/**
* @test
* @covers ::__construct
* @uses \Foundry\Masonry\Core\Task::__construct
*/
public function testConstruct()
{
$task = $this->getMockForAbstractClass(TaskInterface::class);
$description = $this->getMockForAbstractClass(DescriptionInterface::class);
$namedTask = 'someTask';
$parameters = [];
$arrayPool =
$this
->getMockBuilder(ArrayPool::class)
->disableOriginalConstructor()
->setMethods(['addTask', 'addPotentialTask'])
->getMock();
$arrayPool
->expects($this->at(0))
->method('addTask')
->with($task);
$arrayPool
->expects($this->at(1))
->method('addTask')
->with($this->isInstanceOf(TaskInterface::class));
$arrayPool
->expects($this->at(2))
->method('addPotentialTask')
->with($namedTask, $parameters);
$construct = $this->getObjectMethod($arrayPool, '__construct');
$construct([
0=>$task,
1=>$description,
$namedTask => $parameters
]);
}
/**
* Gets returns a proxy for any method of an object, regardless of scope
* @param object $object Any object
* @param string $methodName The name of the method you want to proxy
* @return \Closure
*/
protected function getObjectMethod($object, $methodName)
{
if (!is_object($object)) {
throw new \InvalidArgumentException('Can not get method of non object');
}
$reflectionMethod = new \ReflectionMethod($object, $methodName);
$reflectionMethod->setAccessible(true);
return function () use ($object, $reflectionMethod) {
return $reflectionMethod->invokeArgs($object, func_get_args());
};
}
class ToTest
{
function __construct(){
$this->methodToMock(); // need to mock that for future tests
}
// my methods class
public function methodToMock(){}
}
class ToTestTest{
/**
* @test
* it should do something
*/
public function it_should_do_something(){
$ToTest = \Mockery::mock('ToTest')
->shouldDeferMissing()
->shouldReceive("methodToMock")
->andReturn("someStub")
->getMock();
$this->assertEquals($expectation, $ToTest->methodToMock());
}
}