Mockeryのバグ調査してみる(Mockeryのバグではなく、PHPの関数の仕様説)
調査中のissue
失敗するテストケース
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Mockery;
use Tests\TestCase;
class Foo
{
public function bar($a, ...$b)
{
return [$a, $b];
}
}
class mockeryBugTest extends TestCase
{
/** @test */
public function a_test()
{
$mock = Mockery::mock(Foo::class)->makePartial();
$res = $mock->bar('abc', ...['def' => 'hjk']);
self::assertEquals('abc', $res[0]);
self::assertEquals(['def' => 'hjk'], $res[1]);
}
}
本来であれば、['def' => 'hjk']
が返るはずなのに、なぜか空配列が入ってくる
self::assertEquals(['def' => 'hjk'], $res[1]);
makePartial()
が悪さしてそう
makePartial()
のメソッドは以下の通り
public function makePartial()
{
$this->_mockery_deferMissing = true;
return $this;
}
_mockery_deferMissing
のプロパティをtrueに変更している
そして、Mockインスタンスを返却している
そもそも_mockery_deferMissing
プロパティとはなんなのか
/**
* Flag to indicate whether we can defer method calls missing from our
* expectations
*
* @var bool
*/
protected $_mockery_deferMissing = false;
PHPDocのコメントを日本語訳すると。。
期待から外れたメソッドコールを延期できるかどうかを示すフラグ
期待から外れたメソッド
というのは、shouldReceive()などで振る舞いが指定されていないメソッドのこと
延期できるか
というのは、振る舞いが指定されていないメソッドをそのまま動作させるかどうかだと思う
class Foo
{
public function foo($a)
{
return $a;
}
public function bar($a, ...$b)
{
return [$a, $b];
}
}
$foo = Mockery::mock($foo)->makePartial();
$foo->shouldReceive('foo')->andReturn('Hello World.')
//期待されているメソッドコール(振る舞いが指定されている)
//異なる引数で何回呼び出しても、`Hello World.`の文字列が返却される
$foo->foo('aaa');
//期待から外れたメソッドコール(振る舞いが指定されていない)
//引数が異なれば、返却される値も変わる
$foo->bar('aa' ['bb' => 'cc']);
_mockery_deferMissing
がtrueの時に影響を受ける箇所を洗い出す
該当箇所は2箇所
} elseif ($this->_mockery_deferMissing && is_callable(get_parent_class($this) . '::' . $method)
&& (!$this->hasMethodOverloadingInParentClass() || (get_parent_class($this) && method_exists(get_parent_class($this), $method)))) {
return call_user_func_array(get_parent_class($this) . '::' . $method, $args);
}
} elseif ($this->_mockery_deferMissing && get_parent_class($this) && method_exists(get_parent_class($this), '__call')) {
return call_user_func(get_parent_class($this) . '::__call', $method, $args);
} elseif ($this->_mockery_deferMissing && is_callable("parent::$method")
&& (!$this->hasMethodOverloadingInParentClass() || (get_parent_class($this) && method_exists(get_parent_class($this), $method)))) {
return call_user_func_array("parent::$method", $args);
}
この中に処理が入ってきてそう。
この中で、dd($method, $args)
をしてみる。
本来であれば、以下のような結果が返ってくるはず。
"bar" // $method
array:1 [ //$args
0 => "abc"
1 => array:1 [
"def" => "hjk"
]
]
実際の結果
"bar" // $method
array:1 [ //$args
0 => "abc"
]
ふむ。
この時点で、第二引数が消し去られている。
てことは、makePartial()関係ない説・・・?
さっきのelseifが記述されているのは、以下のメソッド
protected function _mockery_handleMethodCall($method, array $args)
このメソッドに入ってきた時点で、第二引数がなくなっている。
_mockery_handleMethodCall
は、Mockery側で作成されたbarメソッドの中で呼ばれる
public function bar($a, ...$b){
$argc = func_num_args(); // 関数に渡された関数の数
$argv = func_get_args(); //関数に渡された関数の中身
$ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv); // 関数実行
return $ret;
}
func_num_args
とfunc_get_args
の結果を見てみると、以下のようになった。
1 // func_num_args()
array:1 [ // func_get_args()
0 => "abc"
]
引数には2個渡している('abc', ['def' => 'hjk'])のに、1個になっている。
何かがおかしい・・・
Mockeryなしでfunc_num_args
とfunc_get_args
の結果を見てみると、Mockeryありの場合と同じになった。
1 // func_num_args()
array:1 [ // func_get_args()
0 => "abc"
]
``
てことは、Mockery側のバグではなさそう。
どうやら、PHPの関数側の挙動が原因っぽい
この関数は、未知の名前付きの可変長引数を無視します。 未知の名前付き引数は、可変長引数を通じてのみアクセスできます。
どうやって回避しようかな。。
Mockeryのコラボレーターの人もすぐに解決策は思いつかない様子。。