Home > Software design >  PhpUnit: How can I use a mapping for method arguments?
PhpUnit: How can I use a mapping for method arguments?

Time:11-29

I'm using PHPUnit 9.5 to test my code. My class looks like this (simplified):

class MyClass {
  private $lib = null;

  public function __construct(?ExternalLib $lib = null) {
    $this->lib = $lib ?? new ExternalLib();
  }

  public function executeAB($value) {
    $this->lib->init();
    $this->lib->setOption(ExternalLib::OPTION_A, 'x');
    $this->lib->setOption(ExternalLib::OPTION_B, $value);
    // ... even more options which I don't need to test
    $this->lib->execute();
  }

  public function executeA() {
    $this->lib->init();
    $this->lib->setOption(ExternalLib::OPTION_A, 'y');
    $this->lib->execute();
  }

  public function executeB($value) {
    $this->lib->init();
    $this->lib->setOption(ExternalLib::OPTION_B, $value);
    $this->lib->execute();
  }
}

Now I want to check if given options with values are set when the execute methods are called. I have no access to modify "ExternalLib" but I could use a test double. The problem for me is that I want to check option key and value together because they depend on it. PHPUnit only supports method with and withConsecutive, but no withMapping or withCallback. I don't want to use withConsecutive because the order in which the options are set do not matter and there are also more options in those methods which I basically do not need to test in this context.

I think there are multiple options to solve this issue, but none seems optimal.

a) I could use withConsecutive, but then I'll have to check for 7 options when I only need to test 1 or 2. Also if I change the order how the options are set in my base class, the test would fail.

b) Try to collect the options in a global array and validate the options array after execution

public function testExecuteAB() {
  $libMock = $this->getMockBuilder(ExternalLib::class)->disableOriginalConstructor()->getMock();

  $options = [];
  $libMock->expects($this->once())->method('execute');
  $libMock->method('setOption')
    ->with(self::callback(function ($option, $value) use ($options) {
      $options[$option] = $value;
    }));

  $object = new MyClass($libMock);
  $object->executeAB('myValue');
  $this->assertSame('x', $options[ExternalLib::OPTION_A]);
  $this->assertSame('myValue', $options[ExternalLib::OPTION_B]);
}

c) Use a callback and implement a mapping. So my latest try is the code below. Here I'll get a parameter mismatch error, because "with" was called only with one argument (self::callback).

public function testExecuteAB() {
  $libMock = $this->getMockBuilder(ExternalLib::class)->disableOriginalConstructor()->getMock();

  $libMock->expects($this->once())->method('execute');
  $libMock->method('setOption')
    ->with(self::callback(function ($option, $value) {
      if ($option === ExternalLib::OPTION_A) {self::assertSame('x', $value);}
      if ($option === ExternalLib::OPTION_B) {self::assertSame('myValue', $value);}
    }));

  $object = new MyClass($libMock);
  $object->executeAB('myValue');
}

I read the docs at https://phpunit.readthedocs.io/en/9.5/test-doubles.html#test-doubles-mock-objects-examples-subjecttest3-php but couldn't find a solution to my problem. Does someone have an idea to validate if given options are set?

CodePudding user response:

In PHPUnit there is also willReturnCallback(), an example of which can be seen at the bottom of this article. With this method, you will be passed all of the arguments passed to the mocked method and then run assertions on them and also base the return value on these (if required).

With that, you put together the following code which solved your problem. The principle is that as you make each call, you can validate the expected parameters against a defined array of your expectations...

$libMock->method('setOption')
    ->willReturnCallback(function ($option, $value) use ($expectations) 
         { 
             if (isset($expectations[$option])) { 
                 $this->assertSame($expectations[$option], $value);
             } 
         });

Also depending on if all expectations are covered, you could add an assertion which will throw if the isset() fails.

  • Related