Stubbing метод, вызываемый конструктором класса

Как заглушить метод в PHPUnit, который вызывается классом в конструкторе тестов? Простой код ниже, например, не будет работать, потому что к моменту объявления объявленного метода объект-заглушка уже был создан и мой метод был вызван, unubbed.

Класс для проверки:

class ClassA {
  private $dog;
  private $formatted;

  public function __construct($param1) { 
     $this->dog = $param1;       
     $this->getResultFromRemoteServer();
  }

  // Would normally be private, made public for stubbing
  public getResultFromRemoteServer() {
    $this->formatted = file_get_contents('http://whatever.com/index.php?'.$this->dog);
  }

  public getFormatted() {
    return ("The dog is a ".$this->formatted);
  }
}

Тестовый код:

class ClassATest extends PHPUnit_Framework_TestCase {
  public function testPoodle() {  
    $stub = $this->getMockBuilder('ClassA')
                 ->setMethods(array('getResultFromRemoteServer'))
                 ->setConstructorArgs(array('dog52'))
                 ->getMock();

    $stub->expects($this->any())
         ->method('getResultFromRemoteServer')
         ->will($this->returnValue('Poodle'));

    $expected = 'This dog is a Poodle';
    $actual = $stub->getFormatted();
    $this->assertEquals($expected, $actual);
  }
}

Ответ 1

Проблема заключается не в том, чтобы окупить метод, а в ваш класс.

Вы выполняете работу в конструкторе. Чтобы установить объект в состояние, вы извлекаете удаленный файл. Но этот шаг не требуется, поскольку объект не нуждается в том, чтобы данные находились в правильном состоянии. Вам не нужен результат из файла, прежде чем вы назовете getFormatted.

Вы можете отложить загрузку:

class ClassA {
  private $dog;
  private $formatted;

  public function __construct($param1) { 
     $this->dog = $param1;       
  }
  protected getResultFromRemoteServer() {
    if (!$this->formatted) {
        $this->formatted = file_get_contents(
            'http://whatever.com/index.php?' . $this->dog
        );
    }
    return $this->formatted;
  }
  public getFormatted() {
    return ("The dog is a " . $this->getResultFromRemoteServer());
  }
}

чтобы вы лениво загружали удаленный доступ, когда он действительно нужен. Теперь вам вообще не нужно заглушать getResultFromRemoteServer, но вместо этого можно заглушить getFormatted. Вам также не нужно будет открывать ваш API для тестирования и публиковать getResultFromRemoteServer public.

В боковом указателе, даже если это всего лишь пример, я переписал бы этот класс для чтения

class DogFinder
{
    protected $lookupUri;
    protected $cache = array();
    public function __construct($lookupUri)
    {
        $this->lookupUri = $lookupUri;
    }
    protected function findById($dog)
    {
        if (!isset($this->cache[$dog])) {
            $this->cache[$dog] = file_get_contents(
                urlencode($this->lookupUri . $dog)
            );
        }
        return $this->cache[$id];
    }
    public function getFormatted($dog, $format = 'This is a %s')
    {
        return sprintf($format, $this->findById($dog));
    }
}

Поскольку это Finder, теперь может иметь смысл иметь findById public сейчас. Просто держите его в безопасности, потому что это то, что у вас было в вашем примере.


Другим вариантом будет расширение Subject-Under-Test и замена метода getResultFromRemoteServer вашей собственной реализацией, возвращающей Poodle. Это означает, что вы не тестируете фактический ClassA, а подкласс ClassA, но это то, что происходит, когда вы используете Mock API в любом случае.

Как и в случае с PHP7, вы можете использовать класс анонимного типа:

public function testPoodle() {

    $stub = new class('dog52') extends ClassA {
      public function getResultFromRemoteServer() {
          return 'Poodle';
      }
    };

    $expected = 'This dog is a Poodle';
    $actual = $stub->getFormatted();
    $this->assertEquals($expected, $actual);
}

Перед PHP7 вы просто напишите обычный класс, расширяющий тему-под-Тест, и используйте это вместо Subject-Under-Test. Или используйте disableOriginalConstructor, как показано в другом месте на этой странице.

Ответ 2

Используйте disableOriginalConstructor(), чтобы getMock() не вызывал конструктор. Имя немного вводит в заблуждение, потому что вызов этого метода заканчивается передачей false для $callOriginalConstructor. Это позволяет вам задавать ожидания по возвращенному макету перед вызовом конструктора вручную.

$stub = $this->getMockBuilder('ClassA')
             ->setMethods(array('getResultFromRemoteServer'))
             ->disableOriginalConstructor()
             ->getMock();
$stub->expects($this->any())
     ->method('getResultFromRemoteServer')
     ->will($this->returnValue('Poodle'));
$stub->__construct('dog52');
...