PHPunit: Create mock for a non-default constructible class which forwards all method to original except for one

Viewed 25

I am relatively new to mocking and I assume I have a fundamental misunderstand how PHPunit mocks work and how they are supposed to be used. My SUT (subject under test) contains a class which is not default-constructible and for which I want to replace a single method with a mock.

Let's assume the class I want to mock looks like this

class MyClass {
  protected int $someValue;
  protected string $anotherValue;

  public function __construct(array $param1, int $param2) {
    // does something, among other things
    // initializes  $this->someValue and $this->anotherValue
  }

  public function doSomething(string $a): array {
    // ...
  }

  public function doSomethingElse(bool $a): array {
    // ...
  }

  public function isOpen(): bool {
    // here some real logic happens based on the inner state of the object
  }
}

My SUT uses this class throughout the code and creates objects of this class as needed.

I want to proxy this class in the following sense

  • If the SUT creates a new object of the class, a proxy object (i.e. the mock) is created and the proxy class (aka mock) creates an object of MyClass internally forwarding the arguments of the constructor
  • If the SUT calls any method of the object, the original methods (with the correct parameters) are invoked on the original object.
  • If the SUT class isOpen the mock always returns false.

So this is what I tried

class MyTest extends PHPUnitTestClass {
  public function testWithMock: void {
    $proxy = $this->getMockBuilder(MyClass::class)
      ->enableOriginalConstructor()
      ->enableOriginalClone()
      ->enableProxyingToOriginalMethods()
      ->disableArgumentCloning()
      ->getMock();

    $proxy->method('isOpen')->willReturn(false);

    // ... the actual test
  }
}

The code already fails in the first statement, because obviously MockBuilder tries to construct the original class upon getMock. This is not what I need. I don't need a single instance of the original class inside the mock object. I want to create a new object of the original class whenever the SUT calls new and I only want to change the behavior of a single method.

1 Answers

Phpunit alone can't do this for you as when you use the new operator in your code, it can not interfere with it.

One way to deal with it is to make the new disappear by replacing it with a callable that returns the new object. That is a factory function that returns a new object.

Then inject that factory function into the SUT and have it return the (new) mock each time it is called.

You then have branched your production and testing code with a strategy to create the new object: One for production (default) and one for testing.

Use dependency injection to your benefit to make the code test-able while previously it was not at all.

Related