PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHPUnit – unit tests, mocks, data providers, testing in Magento 2

by Henryk Tews / Tuesday, 12 January 2021 / Published in PHP

Unit testing in PHP projects is a topic most developers know in theory but keep putting off until “when there is time”. The time never comes. I show how to get started with PHPUnit, how to write tests that actually check something meaningful, and how to mock dependencies – with examples that look like real production code.

Installation and first test

composer require --dev phpunit/phpunit

# Run tests
vendor/bin/phpunit tests/

# With code coverage (requires Xdebug or PCOV)
vendor/bin/phpunit --coverage-html coverage/ tests/
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new PriceCalculator();
    }

    public function testCalculatesTotalWithTax(): void
    {
        $result = $this->calculator->calculateTotal(100.0, 0.23);
        $this->assertEquals(123.0, $result);
    }

    public function testReturnsPriceWhenTaxIsZero(): void
    {
        $result = $this->calculator->calculateTotal(100.0, 0.0);
        $this->assertEquals(100.0, $result);
    }

    public function testThrowsOnNegativePrice(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->calculator->calculateTotal(-10.0, 0.23);
    }
}

Mocking dependencies

<?php

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

class OrderServiceTest extends TestCase
{
    private MockObject $orderRepository;
    private MockObject $emailNotifier;
    private OrderService $service;

    protected function setUp(): void
    {
        // createMock() generates a class that implements the interface
        // but does nothing by default
        $this->orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $this->emailNotifier   = $this->createMock(EmailNotifierInterface::class);

        $this->service = new OrderService(
            $this->orderRepository,
            $this->emailNotifier
        );
    }

    public function testCancelsOrderAndSendsEmail(): void
    {
        $order = $this->createMock(OrderInterface::class);
        $order->method('getId')->willReturn(42);
        $order->method('getCustomerEmail')->willReturn('customer@example.com');

        // Define what the mock should return
        $this->orderRepository
            ->method('getById')
            ->with(42)
            ->willReturn($order);

        // Assert the mock is called with specific arguments
        $order->expects($this->once())
            ->method('setStatus')
            ->with('cancelled');

        $this->orderRepository->expects($this->once())
            ->method('save')
            ->with($order);

        $this->emailNotifier->expects($this->once())
            ->method('sendCancellationEmail')
            ->with('customer@example.com');

        $this->service->cancelOrder(42);
    }
}

Data providers – test multiple inputs

<?php

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class PriceCalculatorTest extends TestCase
{
    // Data provider - returns sets of [input, expected]
    public static function priceProvider(): array
    {
        return [
            'zero tax'        => [100.0, 0.0,  100.0],
            '23% VAT'         => [100.0, 0.23, 123.0],
            '8% reduced rate' => [100.0, 0.08, 108.0],
            'small amount'    => [0.01,  0.23,  0.0123],
        ];
    }

    #[DataProvider('priceProvider')]
    public function testCalculatesCorrectly(float $price, float $tax, float $expected): void
    {
        $result = (new PriceCalculator())->calculateTotal($price, $tax);
        $this->assertEqualsWithDelta($expected, $result, 0.001);
    }
}

Testing in Magento 2 – integration tests

<?php

use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class ProductRepositoryTest extends TestCase
{
    private \Magento\Catalog\Api\ProductRepositoryInterface $repository;

    protected function setUp(): void
    {
        // Bootstrap Magento Object Manager in tests
        $objectManager    = Bootstrap::getObjectManager();
        $this->repository = $objectManager->get(
            \Magento\Catalog\Api\ProductRepositoryInterface::class
        );
    }

    /**
     * @magentoDataFixture Magento/Catalog/_files/product_simple.php
     */
    public function testGetBySkuReturnsProduct(): void
    {
        $product = $this->repository->get('simple');
        $this->assertEquals('simple', $product->getSku());
        $this->assertEquals(1, $product->getStatus());
    }
}
# Run Magento integration tests
cd dev/tests/integration
../../../vendor/bin/phpunit --config phpunit.xml \
    testsuite/Magento/Catalog/Model/ProductRepositoryTest.php

phpunit.xml – configuration

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         stopOnFailure="false">

    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>
</phpunit>

Summary

Unit tests pay off even in small projects – you catch regressions in minutes instead of after a client call. The key is to start simple: test pure functions and service classes that have a clear, mockable dependency interface. In Magento 2 the Service Contracts architecture with Repository interfaces makes this straightforward – mock the repository, test the service logic.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}