Skip to content
intermediate Phase 73 · Testing Fundamentals

Unit Tests in Magento

1h
1 problems
Topic Progress 0%

PHPUnit Setup

PHPUnit Configuration

<!-- dev/tests/unit/phpunit.xml.dist -->
<?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="bootstrap.php"
         colors="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Magento Test Suite">
            <directory>../phpunit/tests/unit</directory>
        </testsuite>
        <testsuite name="Module Tests">
            <directory>../../../app/code/Vendor/Module/Test/Unit</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory>../../../app/code/Vendor/Module</directory>
        </include>
    </coverage>
</phpunit>

Running Tests

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

# Run specific test suite
vendor/bin/phpunit --testsuite "Module Tests"

# Run specific test class
vendor/bin/phpunit app/code/Vendor/Module/Test/Unit/Model/ProductTest.php

# Generate coverage report
vendor/bin/phpunit --coverage-html dev/tests/unit/coverage app/code/Vendor/Module/Test/Unit/

Bootstrap File

// dev/tests/unit/bootstrap.php
require_once BP . '/vendor/autoload.php';

use Magento\Framework\App\Bootstrap;

$bootstrap = Bootstrap::create(BP, $_SERVER);
$objectManager = $bootstrap->getObjectManager();

Key Points

  • Unit tests isolate individual classes
  • Tests should be fast (milliseconds)
  • Mock all external dependencies
  • Aim for high code coverage (80%+)

Writing Test Cases

Basic Test Structure

namespace Vendor\Module\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Model\Product;

class ProductTest extends TestCase
{
    private $product;

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

    public function testGetName()
    {
        $this->product->setName('Test Product');
        $this->assertEquals('Test Product', $this->product->getName());
    }

    public function testGetPrice()
    {
        $this->product->setPrice(99.99);
        $this->assertEquals(99.99, $this->product->getPrice());
    }

    public function testPriceCannotBeNegative()
    {
        $this->expectException(\Exception::class);
        $this->product->setPrice(-10);
    }
}

Assertion Methods

public function testAssertions()
{
    // Equality
    $this->assertEquals(10, $value);
    $this->assertEqualsCanonicalizing([3, 1, 2], [1, 2, 3]);
    
    // Identity
    $this->assertSame(10, $value);  // Strict === comparison
    
    // Type
    $this->assertIsString($value);
    $this->assertIsArray($value);
    
    // Array
    $this->assertArrayHasKey('key', $array);
    $this->assertContains('value', $array);
    
    // String
    $this->assertStringContainsString('needle', $haystack);
    $this->assertStringStartsWith('prefix', $string);
    
    // Null
    $this->assertNull($value);
    $this->assertNotNull($value);
    
    // Exception
    $this->expectException(\Exception::class);
    $this->expectExceptionMessage('Error message');
}

Key Points

  • Test methods start with 'test'
  • Use setUp() for common initialization
  • Use tearDown() for cleanup
  • One assertion per test method (ideally)

Mocks and Stubs

Creating Mocks

use PHPUnit\Framework\MockObject\MockObject;

class ProductRepositoryTest extends TestCase
{
    private $repository;
    private $connectionMock;

    protected function setUp(): void
    {
        $this->connectionMock = $this->getMockBuilder(
            \Magento\Framework\DB\Adapter\PDO\Adapter::class
        )->disableOriginalConstructor()
         ->getMock();

        $this->repository = new ProductRepository($this->connectionMock);
    }

    public function testFindProduct()
    {
        $expectedProduct = ['entity_id' => 1, 'sku' => 'TEST-001'];

        $this->connectionMock->expects($this->once())
            ->method('fetchRow')
            ->with(
                $this->stringContains('catalog_product_entity'),
                $this->equalTo(['entity_id' => 1])
            )
            ->willReturn($expectedProduct);

        $result = $this->repository->findById(1);

        $this->assertEquals($expectedProduct, $result);
    }
}

Mock Behaviors

// Different return values for multiple calls
$mock->method('getValue')
    ->willReturnOnConsecutiveCalls('first', 'second', 'third');

// Return based on arguments
$mock->method('validate')
    ->willReturnMap([
        ['valid@email.com', true],
        ['invalid-email', false],
        ['', false],
    ]);

// Throw exception
$mock->method('save')
    ->willThrowException(new \Exception('Save failed'));

// Callback
$mock->method('process')
    ->willReturnCallback(function($data) {
        return strtoupper($data);
    });

Key Points

  • Mock external dependencies, not the class under test
  • Use expects() to verify method calls
  • Use with() to specify expected arguments
  • Use willReturn() to define return values

Data Providers

Data Provider Usage

class PriceCalculatorTest extends TestCase
{
    private $calculator;

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

    /**
     * @dataProvider priceCalculationProvider
     */
    public function testCalculatePrice($price, $quantity, $discount, $expected)
    {
        $result = $this->calculator->calculate($price, $quantity, $discount);
        $this->assertEquals($expected, $result, '', 0.01);
    }

    public function priceCalculationProvider(): array
    {
        return [
            'basic calculation' => [100, 2, 0, 200],
            'with discount' => [100, 2, 10, 180],
            'zero quantity' => [100, 0, 0, 0],
            'decimal price' => [19.99, 3, 5, 56.97],
            'high discount' => [100, 1, 50, 50],
        ];
    }
}

External Data Providers

class ValidationTest extends TestCase
{
    /**
     * @dataProvider externalDataProvider
     */
    public function testValidation($input, $expected)
    {
        $this->assertEquals($expected, $this->validate($input));
    }

    public static function externalDataProvider(): array
    {
        return require __DIR__ . '/data/validation-cases.php';
    }
}

// data/validation-cases.php
return [
    'valid email' => ['test@example.com', true],
    'invalid email' => ['not-an-email', false],
    'empty string' => ['', false],
];

Key Points

  • Data providers must be public static methods
  • Return array of arrays with test data
  • Name data cases for clear test output
  • Use external files for large datasets

Practice Problems

0 / 1 solved
Write Unit Tests

Write unit tests for a price calculator class that applies discounts and taxes.

Solution
<?php
namespace Vendor\Module\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Model\PriceCalculator;

class PriceCalculatorTest extends TestCase
{
    private $calculator;

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

    /** @dataProvider priceProvider */
    public function testCalculate($price, $tax, $discount, $expected)
    {
        $result = $this->calculator->calculate($price, $tax, $discount);
        $this->assertEquals($expected, $result, '', 0.01);
    }

    public function priceProvider(): array
    {
        return [
            'no discount' => [100, 20, 0, 120],
            'with discount' => [100, 20, 10, 108],
            'zero price' => [0, 20, 0, 0],
            'high tax' => [100, 50, 0, 150],
        ];
    }
}

Quiz

1. What makes a good unit test?

Question 1 options

2. What is the purpose of mocking?

Question 2 options

3. When should setUp() be used?

Question 3 options

4. What is a data provider?

Question 4 options

Flashcards

Question

Unit test characteristics?

Answer

Fast, isolated, repeatable, single class focus

Question

Mock purpose?

Answer

Replace external dependencies for isolation

Question

setUp() runs when?

Answer

Before each test method

Question

Data provider method?

Answer

Public static method returning array of test data

Revision Notes

Key Takeaways

  • 1. Unit tests isolate single classes with mocked dependencies
  • 2. Use setUp() for initialization, tearDown() for cleanup
  • 3. Mocks verify interactions and replace external dependencies
  • 4. Data providers enable parameterized testing

Interview Tips

  • Explain the difference between mocks and stubs
  • Discuss test-driven development (TDD) benefits
  • Know PHPUnit assertion methods

Cheat Sheet

Magento Unit Tests

  • Run: vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist
  • Mock: getMock(), createMock()
  • Assert: assertEquals, assertSame, assertNull
  • Data: @dataProvider annotation