Skip to content
intermediate Phase 7 · PHP Standards & Tools

PHPUnit: Testing PHP and Magento Applications

Master PHPUnit basics, test cases, assertions, data providers, mocking, and Magento testing patterns.

45m
0 problems
Topic Progress 0%

PHPUnit Basics

Installing PHPUnit

# Via Composer (recommended)
composer require --dev phpunit/phpunit:^9.5

# Run tests
vendor/bin/phpunit
vendor/bin/phpunit tests/
vendor/bin/phpunit --filter testMethodName

Basic Test Case

<?php
namespace Vendor\Module\Test\Unit\Helper;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Helper\Data;

class DataTest extends TestCase
{
    private Data $helper;

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

    public function testFormatPrice(): void
    {
        $result = $this->helper->formatPrice(29.99);
        $this->assertEquals('$29.99', $result);
    }

    public function testFormatPriceWithCurrency(): void
    {
        $result = $this->helper->formatPrice(29.99, 'EUR');
        $this->assertEquals('29.99 EUR', $result);
    }

    public function testFormatPriceZero(): void
    {
        $result = $this->helper->formatPrice(0);
        $this->assertEquals('$0.00', $result);
    }
}

Test Method Naming

<?php
// Method naming: test<Method><Scenario><ExpectedResult>

public function testFormatPriceWithValidPriceReturnsFormattedString(): void {}
public function testFormatPriceWithZeroReturnsZeroDollars(): void {}
public function testFormatPriceWithNegativePriceReturnsNegative(): void {}
public function testGetProductByIdWithExistingProductReturnsProduct(): void {}
public function testGetProductByIdWithNonExistingProductThrowsException(): void {}

Assertions

<?php
// Equality assertions
$this->assertEquals($expected, $actual);       // == (loose)
$this->assertSame($expected, $actual);          // === (strict)
$this->assertNotEquals($expected, $actual);

// Boolean assertions
$this->assertTrue($value);
$this->assertFalse($value);

// Null assertions
$this->assertNull($value);
$this->assertNotNull($value);

// Type assertions
$this->assertIsArray($value);
$this->assertIsString($value);
$this->assertIsInt($value);
$this->assertIsFloat($value);
$this->assertIsBool($value);
$this->assertIsObject($value);

// String assertions
$this->assertStringContainsString('needle', $haystack);
$this->assertStringStartsWith('prefix', $string);
$this->assertStringEndsWith('suffix', $string);

// Array assertions
$this->assertArrayHasKey('key', $array);
$this->assertContains('value', $array);
$this->assertCount(3, $array);

// Exception assertions
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Error message');
$helper->invalidMethod();

// Collection assertions
$this->assertEquals([1, 2, 3], $array);
$this->assertEmpty($array);
$this->assertNotEmpty($array);

setUp and tearDown

<?php
class ProductTest extends TestCase
{
    private Product $product;

    // Called before each test method
    protected function setUp(): void
    {
        $this->product = new Product('Widget', 29.99);
    }

    // Called after each test method
    protected function tearDown(): void
    {
        // Cleanup if needed
    }

    public function testName(): void
    {
        $this->assertEquals('Widget', $this->product->getName());
    }

    public function testPrice(): void
    {
        $this->assertEquals(29.99, $this->product->getPrice());
    }
}

Key Takeaway

PHPUnit test cases extend TestCase. Use descriptive method names (test). setUp() runs before each test, tearDown() after. Use specific assertions for each type of check.

Data Providers and Parameterized Tests

Data Providers

<?php
namespace Vendor\Module\Test\Unit\Helper;

use PHPUnit\Framework\TestCase;

class PriceCalculatorTest extends TestCase
{
    // Data provider returns array of test data
    public function priceProvider(): array
    {
        return [
            'zero price' => [0, 0.0],
            'normal price' => [29.99, 29.99],
            'high price' => [999.99, 999.99],
            'with discount' => [100, 90.0, 10],
        ];
    }

    /**
     * @dataProvider priceProvider
     */
    public function testCalculatePrice(float $input, float $expected, float $discount = 0): void
    {
        $calculator = new PriceCalculator();
        $result = $calculator->calculate($input, $discount);
        $this->assertEquals($expected, $result);
    }
}

Data Providers with Multiple Arguments

<?php
class TaxCalculatorTest extends TestCase
{
    public function taxRateProvider(): array
    {
        return [
            'US rate' => [100, 0.08, 8.0],
            'UK rate' => [100, 0.20, 20.0],
            'zero rate' => [100, 0, 0.0],
            'decimal price' => [29.99, 0.08, 2.40],
        ];
    }

    /**
     * @dataProvider taxRateProvider
     */
    public function testCalculateTax(float $price, float $rate, float $expectedTax): void
    {
        $calculator = new TaxCalculator();
        $tax = $calculator->calculateTax($price, $rate);
        $this->assertEquals($expectedTax, $tax, '', 0.01); // Delta for float comparison
    }
}

External Data Providers

<?php
// Separate data provider class
namespace Vendor\Module\Test\DataProvider;

class ProductDataProvider
{
    public static function validProductData(): array
    {
        return [
            'simple product' => [
                ['name' => 'Widget', 'price' => 29.99, 'sku' => 'WDG-001'],
                true
            ],
            'missing name' => [
                ['name' => '', 'price' => 29.99, 'sku' => 'WDG-001'],
                false
            ],
            'negative price' => [
                ['name' => 'Widget', 'price' => -10, 'sku' => 'WDG-001'],
                false
            ],
        ];
    }
}

// Use in test case
use Vendor\Module\Test\DataProvider\ProductDataProvider;

class ProductValidationTest extends TestCase
{
    /**
     * @dataProvider \Vendor\Module\Test\DataProvider\ProductDataProvider::validProductData
     */
    public function testValidateProduct(array $data, bool $expected): void
    {
        $validator = new ProductValidator();
        $result = $validator->validate($data);
        $this->assertEquals($expected, $result);
    }
}

Grouping Tests

<?php
/**
 * @group unit
 * @group catalog
 */
public function testProductCreation(): void
{
    // ...
}

// Run specific groups
// vendor/bin/phpunit --group=unit
// vendor/bin/phpunit --group=catalog
// vendor/bin/phpunit --exclude-group=slow

Key Takeaway

Data providers allow testing multiple scenarios with different inputs. Each data set is a named array element. Use @dataProvider annotation to link data provider to test method.

Mocking and Magento Testing Patterns

Mocking Basics

<?php
namespace Vendor\Module\Test\Unit\Service;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Service\OrderService;
use Vendor\Module\Api\MailerInterface;
use Vendor\Module\Api\LoggerInterface;

class OrderServiceTest extends TestCase
{
    private OrderService $service;
    private MailerInterface $mailerMock;
    private LoggerInterface $loggerMock;

    protected function setUp(): void
    {
        // Create mock objects
        $this->mailerMock = $this->createMock(MailerInterface::class);
        $this->loggerMock = $this->createMock(LoggerInterface::class);

        // Inject mocks
        $this->service = new OrderService(
            $this->mailerMock,
            $this->loggerMock
        );
    }

    public function testProcessOrderSendsEmail(): void
    {
        // Set up expectations
        $this->mailerMock->expects($this->once())
            ->method('send')
            ->with(
                'customer@example.com',
                $this->stringContains('Order Confirmation')
            )
            ->willReturn(true);

        $this->loggerMock->expects($this->once())
            ->method('info')
            ->with($this->stringContains('Order processed'));

        // Execute
        $this->service->processOrder([
            'email' => 'customer@example.com',
            'total' => 29.99
        ]);
    }

    public function testProcessOrderLogsErrorOnFailure(): void
    {
        $this->mailerMock->expects($this->once())
            ->method('send')
            ->willThrowException(new \Exception('Mail failed'));

        $this->loggerMock->expects($this->once())
            ->method('error')
            ->with($this->stringContains('Failed'));

        $this->service->processOrder([
            'email' => 'customer@example.com',
            'total' => 29.99
        ]);
    }
}

Mock Methods

<?php
// Create mock
$mock = $this->createMock(SomeClass::class);

// Set up method expectations
$mock->expects($this->once())      // Called exactly once
    ->method('methodName')
    ->with('arg1', 'arg2')          // Expected arguments
    ->willReturn('result');          // Return value

// Different expectation matchers
$mock->expects($this->any());       // Called any number of times
$mock->expects($this->never());     // Never called
$mock->expects($this->atLeast(2));  // At least 2 times
$mock->expects($this->exactly(3));  // Exactly 3 times

// Argument matchers
$this->anything()                    // Any argument
$this->equalTo($value)               // Strict equality
$this->stringContains('text')        // String contains
$this->isType('string')              // Type check
$this->logicalNot($matcher)          // Negation
$this->logicalOr($matcher1, $matcher2) // Either matcher

Magento-Specific Testing

<?php
namespace Vendor\Module\Test\Unit\Block;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Block\Product\View;
use Magento\Framework\View\Element\Template\Context;

class ViewTest extends TestCase
{
    public function testGetProductUrl(): void
    {
        // Mock Magento objects
        $context = $this->createMock(Context::class);
        $urlBuilder = $this->createMock(UrlInterface::class);

        $urlBuilder->method('getUrl')
            ->with('catalog/product/view', ['id' => 1])
            ->willReturn('https://store.com/product/1');

        $context->method('getUrlBuilder')
            ->willReturn($urlBuilder);

        $block = new View($context);
        $block->setProductId(1);

        $this->assertEquals(
            'https://store.com/product/1',
            $block->getProductUrl()
        );
    }
}

Magento Test Directory Structure

app/code/Vendor/Module/
├── Test/
│   ├── Unit/                      # Unit tests (isolated)
│   │   ├── Helper/
│   │   │   └── DataTest.php
│   │   ├── Model/
│   │   │   └── ProductTest.php
│   │   └── Service/
│   │       └── OrderServiceTest.php
│   ├── Integration/               # Integration tests (with DB)
│   │   ├── Model/
│   │   │   └── ProductRepositoryTest.php
│   │   └── Api/
│   │       └── ProductApiTest.php
│   └── Fixtures/                  # Test data
│       └── product.php

Running Magento Tests

# Unit tests
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist

# Integration tests
vendor/bin/phpunit -c dev/tests/integration/phpunit.xml.dist

# Specific test file
vendor/bin/phpunit app/code/Vendor/Module/Test/Unit/Helper/DataTest.php

# Specific test method
vendor/bin/phpunit --filter testFormatPrice app/code/Vendor/Module/Test/Unit/

Key Takeaway

Mock objects isolate your code from dependencies. Use expects/with/willReturn to set up expectations. Magento tests follow Unit (isolated) and Integration (with DB) patterns.

Quiz

1. What is the purpose of setUp() in PHPUnit?

Question 1 options

2. What is a data provider in PHPUnit?

Question 2 options

3. What does createMock() do?

Question 3 options

4. What is the difference between assertEquals and assertSame?

Question 4 options

5. What is the @group annotation used for?

Question 5 options

Flashcards

Question

What is PHPUnit?

Answer

The standard testing framework for PHP. Write test cases, use assertions, mock dependencies, and run tests from command line.

Question

What does setUp() do in PHPUnit?

Answer

Called before each test method. Use to initialize objects, create mocks, and set up test fixtures.

Question

What is a data provider?

Answer

A method returning arrays of test data for parameterized tests. Linked with @dataProvider annotation.

Question

What does createMock() create?

Answer

A test double (fake object) that implements the interface/class. Methods return default values unless expectations are set.

Question

What is the difference between assertEquals and assertSame?

Answer

assertEquals: loose comparison (==). assertSame: strict comparison (===). Use assertSame when type matters.

Question

How do you test exceptions?

Answer

Use $this->expectException(ExceptionClass::class) before the code that throws, then $this->expectExceptionMessage('message').

Question

What is mock expectation matching?

Answer

expects($this->once()) for exact count. with() for argument matching. willReturn() for return value.

Question

Where do Magento unit tests go?

Answer

app/code/Vendor/Module/Test/Unit/. Integration tests in Test/Integration/. Fixtures in Test/Fixtures/.

Revision Notes

Key Takeaways

  • 1. Test cases extend PHPUnit\Framework\TestCase
  • 2. Use descriptive method names: test<Method><Scenario><Result>
  • 3. setUp() runs before each test, tearDown() after
  • 4. Data providers enable parameterized testing
  • 5. Mock objects isolate code from dependencies
  • 6. assertEquals (==) vs assertSame (===)
  • 7. Magento tests: Unit (isolated) and Integration (with DB)

Interview Tips

  • Explain the difference between unit and integration tests
  • Describe how mocking works and when to use it
  • Know common PHPUnit assertions
  • Explain the purpose of data providers
  • Describe Magento's test directory structure

Cheat Sheet

PHPUnit Cheat Sheet

Basic Test:

class MyTest extends TestCase {
    public function testSomething(): void {
        $this->assertEquals($expected, $actual);
    }
}

Common Assertions:
assertEquals, assertSame, assertTrue, assertFalse
assertNull, assertNotNull, assertEmpty
assertContains, assertArrayHasKey, assertCount
expectException, expectExceptionMessage

Data Providers:

public function data(): array {
    return [['input', 'expected']];
}
/** @dataProvider data */
public function test($input, $expected): void {}

Mocking:

$mock = $this->createMock(Class::class);
$mock->expects($this->once())
    ->method('foo')
    ->willReturn('bar');

Run:
vendor/bin/phpunit --filter testMethod