Unit Tests Limitations with Magento 2 and PHPUnit 10

There is no way to write a Unit Test for a Magento 2 extension and execute the test outside an installed Magento 2 application. Usually, when I write a Unit Test for a class in Magento 2, I mock all dependencies of the class. It isn't an exception when it comes to auto-generated factories and interfaces.

The typical Unit Test would look like below:

<?php declare(strict_types=1);

namespace Pronko\StoreCredit\Test\Unit\Service;

use Magento\Quote\Api\Data\CartInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\OrderExtensionInterface;
use Magento\Quote\Api\Data\CartExtensionInterface;
use PHPUnit\Framework\TestCase;
use Pronko\StoreCredit\Service\AttributesCopier;

class AttributesCopierTest extends TestCase
{
public function testCopy(): void
{
$amount = 10.99;
$baseAmount = 8.99;
$object = new AttributesCopier();

$cartAttributes = $this->getMockBuilder(CartExtensionInterface::class)
->addMethods([
'getPronkoStoreCreditAmount',
'getBasePronkoStoreCreditAmount',
])
->getMock();
$cartAttributes->expects($this->once())
->method('getPronkoStoreCreditAmount')
->willReturn($amount);
$cartAttributes->expects($this->once())
->method('getBasePronkoStoreCreditAmount')
->willReturn($baseAmount);
$cart = $this->createMock(CartInterface::class);
$cart->expects($this->once())
->method('getExtensionAttributes')
->willReturn($cartAttributes);

$orderAttributes = $this->getMockBuilder(OrderExtensionInterface::class)
->addMethods([
'setPronkoStoreCreditAmount',
'setBasePronkoStoreCreditAmount',
])
->getMock();

$orderAttributes->expects($this->once())
->method('setPronkoStoreCreditAmount')
->with($amount);
$orderAttributes->expects($this->once())
->method('setBasePronkoStoreCreditAmount')
->with($baseAmount);

$order = $this->createMock(OrderInterface::class);
$order->expects($this->once())
->method('setExtensionAttributes')
->with($orderAttributes);

$order->expects($this->once())
->method('getExtensionAttributes')
->willReturn($orderAttributes);

$object->copy($cart, $order);
}
}

The error appears when you run this test outside of the Magento 2 installation:

There was 1 error: 

1) Pronko\StoreCredit\Test\Unit\Service\AttributesCopierTest::testCopy
PHPUnit\Framework\MockObject\Generator\ReflectionException: Class "Magento\Framework\Api\Data\CartExtensionInterface" does not exist

It happens because there is no CartExtensionInterface interface in the source code. It is auto-generated at the time of compilation (usually with the command bin/magento setup:di:compile).

Before PHPUnit 10, it was enough to have the below code in a test:

$cartAttributes = $this->getMockBuilder(CartExtensionInterface::class)
->addMethods([
'getPronkoStoreCreditAmount',
'getBasePronkoStoreCreditAmount',
])
->getMock();

The same problem happens when we try to create a mock for an auto-generated Factory class.

PHPUnit version 10+ no longer provides functionality to mock non-existent classes or interfaces.

Solution

One of the solutions, I imagine, can be to stop running Unit Tests outside of the Magento 2 application. This approach, however, would increase the chances of missing something during Magento 2 extension development, when the extension is located in a separate Git repository.

Alternatively, you may start skipping the execution of Unit Tests, when using GitHub Actions for example, that depend on auto-generated entities. This is my current approach. Still, I write Unit Tests for all source code including classes that depend on the generated folder in Magento 2, execute them locally and then push my changes to remote so it can be executed by CI.