Модульный тест PHP, имитирующий зависимость переменной-члена

Привет, допустим, я хочу протестировать запуск функции из класса A и использую Mockery для имитации внешнего зависимости:

class A {
    protected myB;

    public function __construct(B $param) {
      $this->myB = $param;
    }

    protected function doStuff() {
      return "done";
    }

    public function run() {
      $this->doStuff();

      $this->myB->doOtherStuff();

      return "finished";
    }
}

class B {
    public function doOtherStuff() {
      return "done";
    }
}

Итак, я написал тест так:

public function testRun() {
    $mockB = Mockery::mock('overload:B');
    $mockB->shouldReceive('doOtherStuff')
        ->andReturn("not done");

    $mockA = Mockery::mock(A::class)->makePartial()->shouldAllowMockingProtectedMethods();
    $mockA->shouldReceive('doStuff')->andReturns("done");
    
    $mockA->run();
}

Это вызывает у меня такое исключение: Ошибка: вызов функции-члена doStuff () при null

Я пробовал разные варианты имитации внутренней зависимости B, которая вызывается в функции запуска, но всегда заканчиваюсь исключением.

Что я здесь делаю не так?


person Kingalione    schedule 17.11.2020    source источник


Ответы (1)


Не смейтесь над тем, что вы тестируете. Вставьте издеваемый B в A.

    public function testRun()
    {
        // Arrange
        $mockB = Mockery::mock(B::class);
        $a = new A($mockB);

        // Assert
        $mockB->shouldReceive('doOtherStuff')
            ->once()
            ->andReturn("not done");

        // Act
        $a->run();
    }

Если вы хотите использовать обезьяний патч A, вы все равно можете передать имитацию B в (подробнее: аргументы конструктора в издевательстве):

    public function testRun()
    {
        // Arrange
        $mockB = Mockery::mock(B::class);
        $a     = Mockery::mock(A::class, [$mockB])
            ->makePartial()
            ->shouldAllowMockingProtectedMethods();
        $a->shouldReceive('doStuff')
            ->andReturns('mocked done');

        // Assert
        $mockB->shouldReceive('doOtherStuff')
            ->once()
            ->andReturn('not done');

        // Act
        $a->run();
    }
person Philip Weinke    schedule 17.11.2020
comment
Но что, если я явно хочу имитировать некоторые функции из A вместо использования настоящих? - person Kingalione; 17.11.2020
comment
В основном то же самое. Вы все еще можете внедрить зависимость. Я обновил свой ответ. Надеюсь это поможет. - person Philip Weinke; 17.11.2020
comment
Я также пробовал это решение, и оно дает мне такую ​​ошибку: Mockery \ Exception \ BadMethodCallException: Получено Mockery_4_B :: doOtherStuff (), но никаких ожиданий не было указано - person Kingalione; 17.11.2020
comment
И если я хочу сделать mockB частичным, он пытается вызвать настоящую функцию doOtherStuff вместо имитируемой. Я действительно не понимаю здесь поведение - person Kingalione; 17.11.2020
comment
4 в MockeryAB говорит мне, что вокруг больше ложных объектов, чем в этом примере. Возможно, вы дважды насмехались над B и без всяких ожиданий вводили B в A. Тестовый пример в моем ответе хорошо работает с классами A и B, показанными в вашем вопросе. - person Philip Weinke; 17.11.2020