web-dev-qa-db-ja.com

PHPUnitのモック-異なる引数を持つ同じメソッドの複数の構成

PHPUnitモックをこのように構成することは可能ですか?

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->any())
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

PHPUnit 3.5.10を使用していますが、「ロガー」引数を想定しているため、マッチャーを要求すると失敗します。 2番目の期待は最初の期待を書き換えるようなものですが、モックをダンプすると、すべてが正常に見えます。

49

PHPUnit 3.6以降、 $this->returnValueMap() があり、メソッドスタブに指定されたパラメーターに応じて異なる値を返すために使用できます。

30
leeb

悲しいことに、これはデフォルトのPHPUnit Mock APIでは不可能です。

私はあなたをこのようなものに近づけることができる2つのオプションを見ることができます:

-> at($ x)の使用

_$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->at(0))
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->at(1))
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));
_

これは問題なく動作しますが、必要以上にテストしています(主に、最初にmatcherで呼び出され、実装の詳細です)。

また、各関数に対して複数の呼び出しがある場合、これは失敗します。


両方のパラメーターを受け入れ、returnCallBackを使用する

これはより多くの作業ですが、呼び出しの順序に依存しないため、よりうまく機能します。

作業例:

_<?php

class FooTest extends PHPUnit_Framework_TestCase {


    public function testX() {

        $context = $this->getMockBuilder('Context')
           ->getMock();

        $context->expects($this->exactly(2))
           ->method('offsetGet')
           ->with($this->logicalOr(
                     $this->equalTo('Matcher'), 
                     $this->equalTo('Logger')
            ))
           ->will($this->returnCallback(
                function($param) {
                    var_dump(func_get_args());
                    // The first arg will be Matcher or Logger
                    // so something like "return new $param" should work here
                }
           ));

        $context->offsetGet("Matcher");
        $context->offsetGet("Logger");


    }

}

class Context {

    public function offsetGet() { echo "org"; }
}
_

これは出力します:

_/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.

array(1) {
  [0]=>
  string(7) "Matcher"
}
array(1) {
  [0]=>
  string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb

OK (1 test, 1 assertion)
_

マッチャーで$this->exactly(2)を使用して、これが呼び出しのカウントにも機能することを示しました。もちろん、それを必要としない場合は、$this->any()に交換することで問題なく動作します。

61
edorian

これはコールバックで実現できます。

_class MockTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExpectedInstance
     */
    public function testMockReturnsInstance($expectedInstance)
    {
        $context = $this->getMock('Context');

        $context->expects($this->any())
           ->method('offsetGet')
           // Accept any of "Matcher" or "Logger" for first argument
           ->with($this->logicalOr(
                $this->equalTo('Matcher'),
                $this->equalTo('Logger')
           ))
           // Return what was passed to offsetGet as a new instance
           ->will($this->returnCallback(
               function($arg1) {
                   return new $arg1;
               }
           ));

       $this->assertInstanceOf(
           $expectedInstance,
           $context->offsetGet($expectedInstance)
       );
    }
    public function provideExpectedInstance()
    {
        return array_chunk(array('Matcher', 'Logger'), 1);
    }
}
_

Context MockのoffsetGetメソッドに渡される「Logger」または「Matcher」引数を渡す必要があります。

_F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 4 assertions)
_

ご覧のとおり、PHPUnitは2つのテストを実行しました。 dataProvider値ごとに1つ。そして、それらの各テストでは、with()のアサーションとinstanceOfのアサーション、つまり4つのアサーションを作成しました。

7
Gordon

@edorianの回答と、メソッドがMatcherとLoggerの両方で呼び出され、MatcherとLoggerのどちらかで2回呼び出されないようにすることに関するコメント(@MarijnHuizendveld)に続いて、以下に例を示します。

$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
       ->method('offsetGet')
       ->with($this->logicalOr(
                 $this->equalTo('Matcher'), 
                 $this->equalTo('Logger')
        ))
       ->will($this->returnCallback(
            function($param) use (&$expectedArguments){
                if(($key = array_search($param, $expectedArguments)) !== false) {
                    // remove called argument from list
                    unset($expectedArguments[$key]);
                }
                // The first arg will be Matcher or Logger
                // so something like "return new $param" should work here
            }
       ));

// perform actions...

// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');

これは、PHPUnit 3.7で使用できます。

テストするメソッドが実際には何も返さず、正しい引数で呼び出されていることをテストする必要があるだけの場合、同じアプローチが適用されます。このシナリオでは、ウィルのreturnCallbackではなく、withへの引数として$ this-> callbackのコールバック関数を使用してこれを実行しようとしました。これは失敗します。内部でphpunitがコールバックtwiceを呼び出しているため、引数マッチャーコールバックを検証しているためです。つまり、2番目の呼び出しでは、引数が予期される引数配列から既に削除されているため、アプローチは失敗します。 phpunitがなぜそれを2回呼び出すのかわからない(不要な無駄のようです)、2番目の呼び出しでそれを削除するだけでそれを回避できると思いますが、これが意図されている一貫したphpunitの動作であることを確信していませんでしたその発生に依存します。

5
crysallus

トピックに対する私の2セント:at($ x)を使用するときは注意してください:これは、期待されるメソッド呼び出しがモックオブジェクトの($ x + 1)番目のメソッド呼び出しになることを意味します。それが期待されるメソッドの($ x + 1)番目の呼び出しになるという意味ではありません。これは私に時間を浪費させたので、あなたと一緒にならないことを願っています。みなさん、よろしくお願いします。

3

私はこれに偶然遭遇しましたPHPモックオブジェクトの拡張: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object

2
powtac

doublit ライブラリを使用したいくつかのソリューションもここにあります:

解決策1:Stubs::returnValueMapを使用する

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument is "Matcher"
    // Return "new Logger()" when first argument is "Logger"
    ->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));

解決策2:コールバックを使用する

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument $arg is "Matcher"
    // Return "new Logger()" when first argument $arg is "Logger"
    ->stub(function($arg){
        if($arg == 'Matcher'){
            return new Matcher();
        } else if($arg == 'Logger'){
            return new Logger();
        }
    });
0
gealex