Skip to content
intermediate Phase 26 · Module Complete

Testing Magento Modules

Guide to testing Magento 2 modules: unit tests, integration tests, test fixtures, test configuration, and writing effective tests

45m
0 problems
Topic Progress 0%

Unit Tests

Test Directory Structure

Amazon/Prep/
├── Test/
│   ├── Unit/
│   │   ├── Model/
│   │   │   └── WarrantyTest.php
│   │   └── Service/
│   │       └── WarrantyServiceTest.php
│   └── Integration/
│       ├── Model/
│       │   └── WarrantyRepositoryTest.php
│       └── _files/
│           └── warranty_data.php
└── phpunit.xml

Basic Unit Test

<?php
namespace Amazon\Prep\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use Amazon\Prep\Model\Warranty;

class WarrantyTest extends TestCase
{
    private Warranty $warranty;

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

    public function testGetNameReturnsName(): void
    {
        $this->warranty->setName('Premium Warranty');
        $this->assertEquals('Premium Warranty', $this->warranty->getName());
    }

    public function testGetDurationReturnsInteger(): void
    {
        $this->warranty->setDuration(24);
        $this->assertEquals(24, $this->warranty->getDuration());
    }

    public function testDefaultStatusIsActive(): void
    {
        $this->warranty->setStatus('active');
        $this->assertEquals('active', $this->warranty->getStatus());
    }
}

Unit Test with Mocks

<?php
namespace Amazon\Prep\Test\Unit\Service;

use PHPUnit\Framework\TestCase;
use Amazon\Prep\Service\WarrantyService;
use Amazon\Prep\Model\WarrantyFactory;
use Amazon\Prep\Model\Warranty;
use Amazon\Prep\Model\ResourceModel\Warranty as WarrantyResource;
use Psr\Log\LoggerInterface;

class WarrantyServiceTest extends TestCase
{
    private WarrantyService $service;
    private $factoryMock;
    private $resourceMock;
    private $loggerMock;

    protected function setUp(): void
    {
        $this->factoryMock = $this->createMock(WarrantyFactory::class);
        $this->resourceMock = $this->createMock(WarrantyResource::class);
        $this->loggerMock = $this->createMock(LoggerInterface::class);

        $this->service = new WarrantyService(
            $this->factoryMock,
            $this->resourceMock,
            $this->loggerMock
        );
    }

    public function testCreateWarrantyReturnsWarranty(): void
    {
        $warranty = $this->createMock(Warranty::class);
        $warranty->method('getId')->willReturn(1);

        $this->factoryMock->method('create')->willReturn($warranty);
        $this->resourceMock->method('save')->willReturnSelf();

        $result = $this->service->createWarranty('Test', 12);

        $this->assertInstanceOf(Warranty::class, $result);
        $this->assertEquals(1, $result->getId());
    }

    public function testCreateWarrantySetsName(): void
    {
        $warranty = $this->createMock(Warranty::class);
        $warranty->expects($this->once())
            ->method('setName')
            ->with('Premium Warranty')
            ->willReturnSelf();

        $this->factoryMock->method('create')->willReturn($warranty);
        $this->resourceMock->method('save')->willReturnSelf();

        $this->service->createWarranty('Premium Warranty', 24);
    }
}

Mock Methods

// Create mock
$mock = $this->createMock(ClassName::class);

// Set expectations
$mock->method('methodName')->willReturn('value');
$mock->expects($this->once())->method('methodName');
$mock->expects($this->exactly(3))->method('methodName');

// Argument matching
$mock->method('methodName')->with($this->equalTo('arg'));
$mock->method('methodName')->with($this->anything());
$mock->method('methodName')->with($this->stringContains('test'));

Integration Tests

Integration Test Setup

<?php
namespace Amazon\Prep\Test\Integration\Model;

use PHPUnit\Framework\TestCase;
use Magento\TestFramework\Helper\Bootstrap;
use Amazon\Prep\Api\WarrantyRepositoryInterface;
use Amazon\Prep\Api\Data\WarrantyInterfaceFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;

class WarrantyRepositoryTest extends TestCase
{
    private WarrantyRepositoryInterface $repository;
    private WarrantyInterfaceFactory $warrantyFactory;
    private SearchCriteriaBuilder $searchCriteriaBuilder;

    protected function setUp(): void
    {
        $this->repository = Bootstrap::getObjectManager()
            ->get(WarrantyRepositoryInterface::class);
        $this->warrantyFactory = Bootstrap::getObjectManager()
            ->get(WarrantyInterfaceFactory::class);
        $this->searchCriteriaBuilder = Bootstrap::getObjectManager()
            ->get(SearchCriteriaBuilder::class);
    }

    public function testSaveAndGetWarranty(): void
    {
        $warranty = $this->warrantyFactory->create();
        $warranty->setName('Integration Test Warranty');
        $warranty->setDuration(12);
        $warranty->setSku('TEST-001');
        $warranty->setStatus(1);

        $savedWarranty = $this->repository->save($warranty);

        $this->assertNotNull($savedWarranty->getId());

        $loadedWarranty = $this->repository->get($savedWarranty->getId());
        $this->assertEquals('Integration Test Warranty', $loadedWarranty->getName());

        // Cleanup
        $this->repository->delete($savedWarranty);
    }

    public function testGetListReturnsResults(): void
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('status', 1)
            ->setPageSize(10)
            ->create();

        $searchResults = $this->repository->getList($searchCriteria);

        $this->assertIsArray($searchResults->getItems());
        $this->assertGreaterThanOrEqual(0, $searchResults->getTotalCount());
    }
}

Integration Test with Fixtures

<?php
namespace Amazon\Prep\Test\Integration\Model;

use Magento\TestFramework\Fixture\RevertIsolation;

class WarrantyServiceTest extends TestCase
{
    /**
     * @fixture Amazon_Prep/Model/WarrantyFixture
     */
    public function testProcessWarranty(): void
    {
        // Test with fixture data
    }
}

Custom Fixture

<?php
namespace Amazon\Prep\Test\Integration\_files;

use Magento\TestFramework\Fixture\FixtureInterface;

class WarrantyFixture implements FixtureInterface
{
    public function __construct(
        private \Amazon\Prep\Api\Data\WarrantyInterfaceFactory $factory,
        private \Amazon\Prep\Api\WarrantyRepositoryInterface $repository
    ) {}

    public function load(): void
    {
        $warranty = $this->factory->create();
        $warranty->setName('Fixture Warranty');
        $warranty->setDuration(12);
        $warranty->setSku('FIXTURE-001');
        $this->repository->save($warranty);
    }
}

Test Configuration

phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
         bootstrap="../../../vendor/autoload.php"
         colors="true"
         verbose="true">
    <testsuites>
        <testsuite name="Unit Tests">
            <directory suffix="Test.php">Test/Unit</directory>
        </testsuite>
        <testsuite name="Integration Tests">
            <directory suffix="Test.php">Test/Integration</directory>
        </testsuite>
    </testsuites>

    <coverage>
        <include>
            <directory suffix=".php">Model</directory>
            <directory suffix=".php">Service</directory>
            <directory suffix=".php">Api</directory>
        </include>
    </coverage>
</phpunit>

Running Tests

# Unit tests only
bin/magento dev:tests:run-unit --filter Amazon_Prep

# Or with PHPUnit directly
./vendor/bin/phpunit -c dev/tests/unit/phpunit.xml

# Integration tests
bin/magento dev:tests:run --filter Amazon_Prep

# Specific test class
./vendor/bin/phpunit Test/Unit/Model/WarrantyTest.php

# With coverage
./vendor/bin/phpunit --coverage-html=coverage Test/Unit/

Test Helper Methods

// Assert equals
$this->assertEquals($expected, $actual);

// Assert contains
$this->assertStringContainsString('needle', $haystack);

// Assert count
$this->assertCount(3, $array);

// Assert exception
$this->expectException(NoSuchEntityException::class);
$this->repository->get(999);

// Assert no exception
$this->repository->get(1); // Should not throw

Testing Best Practices

Test Naming

// Method name: test + MethodBeingTested + Scenario + ExpectedResult
public function testGetNameReturnsNameWhenSet(): void
public function testSaveThrowsExceptionWhenInvalidData(): void
public function testGetListReturnsEmptyWhenNoRecords(): void

AAA Pattern

public function testCreateWarranty(): void
{
    // Arrange
    $name = 'Test Warranty';
    $duration = 12;

    // Act
    $result = $this->service->createWarranty($name, $duration);

    // Assert
    $this->assertEquals($name, $result->getName());
    $this->assertEquals($duration, $result->getDuration());
}

Test Data Builders

class WarrantyBuilder
{
    private string $name = 'Default Warranty';
    private int $duration = 12;
    private string $sku = 'DEFAULT-001';

    public function withName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function withDuration(int $duration): self
    {
        $this->duration = $duration;
        return $this;
    }

    public function build(): WarrantyInterface
    {
        $warranty = new Warranty();
        $warranty->setName($this->name);
        $warranty->setDuration($this->duration);
        $warranty->setSku($this->sku);
        return $warranty;
    }
}

// Usage:
$warranty = (new WarrantyBuilder())
    ->withName('Premium')
    ->withDuration(24)
    ->build();

Common Test Scenarios

  1. Happy path — normal expected behavior
  2. Edge cases — empty inputs, zero values
  3. Error cases — invalid data, missing records
  4. Boundary values — min/max limits
  5. Null handling — null inputs and outputs

CI/CD Integration

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Tests
        run: ./vendor/bin/phpunit -c phpunit.xml

Test Coverage Goals

  • Unit tests: 80%+ line coverage
  • Integration tests: critical paths
  • Focus on business logic, not framework code

Quiz

1. What is the difference between unit and integration tests?

Question 1 options

2. What does setUp() do in PHPUnit?

Question 2 options

3. How do you run only unit tests for a module?

Question 3 options

Flashcards

Question

What does PHPUnit's assert assertEquals do?

Answer

Checks if two values are equal

Question

Where do unit tests live?

Answer

Test/Unit/ directory in the module

Question

What is a test fixture?

Answer

Predefined test data and state setup for tests

Question

How do you mock a class in PHPUnit?

Answer

$this->createMock(ClassName::class)

Question

What is the AAA pattern?

Answer

Arrange, Act, Assert — structure for test methods

Revision Notes

Key Takeaways

  • 1. Unit tests mock dependencies; integration tests use real objects
  • 2. Place tests in Test/Unit/ and Test/Integration/ directories
  • 3. Use setUp() for test initialization, assert methods for verification
  • 4. Follow AAA pattern: Arrange, Act, Assert
  • 5. Aim for 80%+ unit test coverage on business logic

Interview Tips

  • Know the difference between unit and integration tests
  • Be ready to write a test with mocks for a service class
  • Discuss test coverage and testing strategies
  • Explain the AAA test pattern

Cheat Sheet

Unit test:
  extends TestCase
  $this->createMock()
  $this->assertEquals()
  $this->expectException()

Integration test:
  Bootstrap::getObjectManager()
  Test with real DB and services

Run:
  ./vendor/bin/phpunit Test/Unit/
  bin/magento dev:tests:run-unit