Подтвердить что ты не робот

Unit test для издевательства метода, вызванного новым объектом класса

Я пишу unit test для существующего кода, который похож на этот

class someClass {
    public function __construct() { ... }

    public function someFoo($var) {
        ...
        $var = "something";
        ...
        $model = new someClass();
        model->someOtherFoo($var);
    }

    public someOtherFoo($var){
         // some code which has to be mocked
    }
}

Здесь, как я должен быть в состоянии издеваться над вызовом функции "someOtherFoo", чтобы он не выполнял "some code" внутри someOtherFoo?

class someClassTest {
   public function someFoo() {
      $fixture = $this->getMock('someClass ', array('someOtherFoo'));
      $var = "something";
      ....
      // How to mock the call to someOtherFoo() here
   }

}

Можно ли издеваться над конструктором так, чтобы он возвращал мою собственную сконструированную функцию или переменную?

Спасибо

4b9b3361

Ответ 1

Где бы вы ни находились new XXX(...) в тестируемом методе, вы обречены. Извлеките экземпляр нового метода - createSomeClass(...) - того же класса. Это позволяет вам создать частичный макет тестируемого класса, который возвращает значение stubbed или mock из нового метода.

class someClass {
    public function someFoo($var) {
        $model = $this->createSomeClass();  // call method instead of using new
        model->someOtherFoo($var);
    }

    public function createSomeClass() {  // now you can mock this method in the test
        return new someClass();
    }

    public function someOtherFoo($var){
         // some code which has to be mocked
    }
}

В тесте mock createSomeClass() в экземпляре, на который вы вызываете someFoo(), и mock someOtherFoo() в экземпляре, который вы возвращаете из первого издевающегося вызова.

function testSomeFoo() {
    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $created = $this->getMock('someClass', array('someOtherFoo'));
    $created->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // mock createSomeClass() to return the mock above
    $creator = $this->getMock('someClass', array('createSomeClass'));
    $creator->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($created));

    // call someFoo() with the correct $arg
    $creator->someFoo('foo');
}

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

function testSomeFoo() {
    $fixture = $this->getMock('someClass', array('createSomeClass', 'someOtherFoo'));

    // mock createSomeClass() to return the mock
    $fixture->expects($this->once())
            ->method('createSomeClass')
            ->will($this->returnValue($fixture));

    // mock someOtherFoo() to ensure it gets the correct value for $arg
    $fixture->expects($this->once())
            ->method('someOtherFoo')
            ->with('foo');

    // call someFoo() with the correct $arg
    $fixture->someFoo('foo');
}

Ответ 2

Вы можете поставить префикс имени вашего псевдонима с помощью overload:

Проверьте документы на насмешливых жестких зависимостей.

Ваш пример будет что-то вроде:

/**
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
class SomeClassTest extends \PHPUnit\Framework\TestCase
{
    public function test_some_foo()
    {
        $someOtherClassMock = \Mockery::mock('overload:SomeOtherClass');
        $someOtherClassMock->shouldReceive('someOtherFoo')
            ->once()
            ->with('something')
            ->andReturn();

        $systemUnderTest = new SomeClass();

        $systemUnderTest->someFoo('something');
    }

}

Я добавил аннотацию @runTestsInSeparateProcesses потому что обычно @runTestsInSeparateProcesses класс будет использоваться и в других тестах. Без аннотации произойдет сбой автозагрузчика, поскольку в class already exists ошибка.

Если это единственное место, где макетируемый класс используется в вашем наборе тестов, то вы должны удалить аннотацию.

Ответ 3

Я нашел свой путь здесь, пытаясь выполнить white-box тест класса __constructor, чтобы убедиться, что он вызывает метод класса сам по себе, а некоторые данные передаются в __constructor.

В случае, если кто-то другой по той же причине, я думал, что поделился бы тем методом, в котором я использовал (без метода factory -style createSomeClass(), используемого в этом вопросе).

<?php
class someClass {

  public function __constructor($param1) {
    // here is the method in the constructor we want to call
    $this->someOtherFoo($param1);
  }

  public function someOtherFoo($var){  }

}

Теперь тест PHPUnit:

<?php
$paramData = 'someData';

// set up the mock class here
$model = $this->getMock('someClass', 
  array('someOtherFoo'), // override the method we want to check
  array($paramData) // we need to pass in a parameter to the __constructor
);

// test that someOtherFoo() is called once, with out test data
$model->expects($this->once())
      ->with($paramData)
      ->method('someOtherFoo');

// directly call the constructor, instead of doing "new someClass" like normal
$model->__construct($paramData);