2

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?

Urst
  • 21
  • 3

1 Answers1

1

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.

hakre
  • 193,403
  • 52
  • 435
  • 836
  • I also thought of that but this would mean that if a DTO consists of multiple (many) ValueObjects (which can easily be possible in my opinion) we would need to mock **every** ValueObject when creating the DTO mock even though we might only need one for our current test, right? And for not mocking DTOs at all: I prefer to mock everything but the class I want to test because then we are not depending on the other stuff working as intended. – Urst Jun 05 '22 at 11:04
  • @Urst: That is no hard rule, just preference. If you prefer to use Mocks, it needs the Mockbuilder and properly building the mocks not overriding the constructor and passing constructor arguments. – hakre Jun 05 '22 at 11:23