Reputation: 21
before PHP 8.1 we would have something like this:
<?php
declare(strict_types=1);
class Consumer
{
public function __construct(private DataTransferObject $dto)
{
}
public function getName(): string
{
if ($this->dto->getValueOne()->isValid()) {
return 'Adam';
}
return 'Eve';
}
}
class DataTransferObject
{
public function __construct(private ValueObjectOne $valueOne, private ValueObjectTwo $valueTwo)
{
}
public function getValueOne(): ValueObjectOne
{
return $this->valueOne;
}
public function getValueTwo(): ValueObjectTwo
{
return $this->valueTwo;
}
}
Which can easily be tested like so:
class ConsumerTest
{
public function testNameIsCorrect()
{
$valueOneMock = $this->createMock(ValueObjectOne::class);
$dtoMock = $this->createMock(DataTransferObject::class);
$dtoMock->expects($this->once())->method('getValueOne')->willReturn($valueOneMock);
$consumer = new Consumer($dtoMock);
$name = $consumer->getName();
// ...
}
}
Now PHP 8.1 introduced readonly
properties to get rid of boilerplate code. Our example would now look like following:
<?php
declare(strict_types=1);
class Consumer
{
public function __construct(private readonly DataTransferObject $dto)
{
}
public function getName(): string
{
if ($this->dto->valueOne->isValid()) {
return 'Adam';
}
return 'Eve';
}
}
class DataTransferObject
{
public function __construct(public readonly ValueObjectOne $valueOne, public readonly ValueObjectTwo $valueTwo)
{
}
}
Now my question would be how to make this testable? The following would result in call to method isValid
on null
class ConsumerTest
{
public function testNameIsCorrect()
{
$valueOneMock = $this->createMock(ValueObjectOne::class);
$dtoMock = $this->createMock(DataTransferObject::class);
// We no longer need/can mock this method because it's no longer needed
// $dtoMock->expects($this->once())->method('getValueOne')->willReturn($valueOneMock);
$consumer = new Consumer($dtoMock);
$name = $consumer->getName();
// ...
}
}
And trying to assign a value to the public readonly
property for the mock obviously will result in Cannot initialize readonly property ... from scope ...*.
class ConsumerTest
{
public function testNameIsCorrect()
{
$valueOneMock = $this->createMock(ValueObjectOne::class);
$dtoMock = $this->createMock(DataTransferObject::class);
$dtoMock->valueOne = $valueOneMock;
$consumer = new Consumer($dtoMock);
$name = $consumer->getName();
// ...
}
}
Any ideas what the best solution for this issue is?
Upvotes: 2
Views: 3756
Reputation: 198119
Phpunits createMock()
method comes with a default configuration of the mock that disables the original constructor and therefore these properties aren't specifically initialized and have their default NULL
value (maybe not 100% correct, later PHP versions may even start to throw).
Instead use the MockBuilder without disabling the constructor to perform the initialization you need for your test.
Alternatively verify - as these are DTOs - if you need to mock them at all. Just suggesting this as I normally prefer to not mock at all writing tests, so I would perhaps verify this first and only if not possible continue with mocking.
Upvotes: 2