Skip to content
intermediate Phase 12 · Testing & Quality

Mock Objects & Test Doubles

Mock objects, stubs, test doubles, Mockery/PHPUnit mocking, and testing with dependencies

45m
0 problems
Topic Progress 0%

Types of Test Doubles

Test Double Hierarchy

Type Purpose Verifies
Dummy Passed around, never used Nothing
Stub Provides canned answers State
Mock Verifies interactions Behavior
Spy Records calls for later verification Behavior (after the fact)
Fake Working implementation, simplified State

Dummy

// Just needs to exist, never actually used
class OrderProcessorTest extends TestCase
{
    public function testSomething(): void
    {
        $dummy = $this->createMock(LoggerInterface::class);
        $processor = new OrderProcessor($dummy);
        // Logger is passed but never called
    }
}

Stub

// Provides canned responses
class ProductFinderTest extends TestCase
{
    public function testFindReturnsProduct(): void
    {
        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('get')
            ->willReturn(new Product(['sku' => 'TEST', 'name' => 'Test Product']));

        $finder = new ProductFinder($stub);
        $product = $finder->findBySku('TEST');

        $this->assertEquals('TEST', $product->getSku());
    }
}

Mock

// Verifies the method was called with specific arguments
class EmailNotifierTest extends TestCase
{
    public function testSendConfirmationUsesMailer(): void
    {
        $mock = $this->createMock(MailerInterface::class);
        $mock->expects($this->once())
            ->method('send')
            ->with(
                $this->equalTo('customer@example.com'),
                $this->stringContains('Order Confirmation')
            )
            ->willReturn(true);

        $notifier = new EmailNotifier($mock);
        $notifier->sendConfirmation($order);
        // Verifies send() was called exactly once with expected args
    }
}

PHPUnit Mocking in Depth

Creating Mocks

// Basic mock
$mock = $this->createMock(SomeInterface::class);

// Mock with partial stubbing (real methods + overrides)
$mock = $this->getMockBuilder(ProductRepository::class)
    ->disableOriginalConstructor()
    ->onlyMethods(['get', 'save']) // Only stub these methods
    ->getMock();

// Mock that calls real methods except specified ones
$mock = $this->getMockBuilder(ProductRepository::class)
    ->disableOriginalConstructor()
    ->addMethods(['customMethod']) // Add fake method
    ->getMock();

Setting Expectations

$mock = $this->createMock(ProductRepositoryInterface::class);

// Method called exactly once
$mock->expects($this->once())
    ->method('save');

// Method called with specific arguments
$mock->expects($this->once())
    ->method('save')
    ->with($this->callback(function ($product) {
        return $product->getSku() === 'TEST-SKU';
    }));

// Method called any number of times
$mock->expects($this->any())
    ->method('get');

// Method never called
$mock->expects($this->never())
    ->method('delete');

// Method called at least once
$mock->expects($this->atLeastOnce())
    ->method('save');

Return Values

// Return a value
$mock->method('get')->willReturn($product);

// Return different values on successive calls
$mock->method('get')
    ->willReturnOnConsecutiveCalls($product1, $product2, null);

// Return based on arguments
$mock->method('get')
    ->willReturnCallback(function ($sku) {
        return match ($sku) {
            'SKU-1' => $product1,
            'SKU-2' => $product2,
            default => null,
        };
    });

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

Spy Pattern

// Record calls for later verification
$spy = $this->createMock(MailerInterface::class);
$emails = [];
$spy->method('send')
    ->willReturnCallback(function ($to, $subject, $body) use (&$emails) {
        $emails[] = ['to' => $to, 'subject' => $subject];
        return true;
    });

$notifier = new EmailNotifier($spy);
$notifier->sendOrderConfirmation($order1);
$notifier->sendOrderConfirmation($order2);

// Verify after the fact
$this->assertCount(2, $emails);
$this->assertEquals('customer@example.com', $emails[0]['to']);

Testing with Dependencies in Magento

Mocking Magento Services

namespace Vendor\Catalog\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use Vendor\Catalog\Model\ProductImportService;

class ProductImportServiceTest extends TestCase
{
    private ProductImportService $service;
    private $productRepo;
    private $validator;
    private $logger;

    protected function setUp(): void
    {
        $this->productRepo = $this->createMock(
            \Magento\Catalog\Api\ProductRepositoryInterface::class
        );
        $this->validator = $this->createMock(
            \Vendor\Catalog\Api\ProductValidatorInterface::class
        );
        $this->logger = $this->createMock(
            \Psr\Log\LoggerInterface::class
        );

        $this->service = new ProductImportService(
            $this->productRepo,
            $this->validator,
            $this->logger
        );
    }

    public function testImportValidProductSucceeds(): void
    {
        $this->validator->method('validate')
            ->willReturn(true);

        $this->productRepo->expects($this->once())
            ->method('save')
            ->with($this->callback(function ($product) {
                return $product->getSku() === 'NEW-SKU';
            }));

        $this->logger->expects($this->once())
            ->method('info')
            ->with($this->stringContains('imported')); // Check log message

        $this->service->import([
            'sku' => 'NEW-SKU',
            'name' => 'New Product',
            'price' => 29.99,
        ]);
    }

    public function testImportInvalidProductLogsError(): void
    {
        $this->validator->method('validate')
            ->willReturn(false);

        $this->productRepo->expects($this->never())
            ->method('save');

        $this->logger->expects($this->once())
            ->method('warning')
            ->with($this->stringContains('validation failed'));

        $this->service->import([
            'sku' => '', // Invalid: empty SKU
            'name' => '',
            'price' => -1,
        ]);
    }
}

Mockery (Alternative)

use Mockery;
use Mockery\PHPUnit\MockeryTrait;

class ProductTest extends TestCase
{
    use MockeryTrait;

    public function testProductUsesRepository(): void
    {
        $repo = Mockery::mock(\Magento\Catalog\Api\ProductRepositoryInterface::class);
        $repo->shouldReceive('save')
            ->once()
            ->with(Mockery::on(fn($product) => $product->getSku() === 'TEST'))
            ->andReturn(true);

        $service = new ProductService($repo);
        $service->save(new Product(['sku' => 'TEST']));
    }

    protected function tearDown(): void
    {
        Mockery::close();
    }
}

Quiz

1. What is the difference between a mock and a stub?

Question 1 options

2. PHPUnit's expects($this->once()) means:

Question 2 options

3. When should you use a mock vs a real object?

Question 3 options

Flashcards

Question

Stub vs Mock?

Answer

Stub = provides canned data. Mock = verifies method calls.

Question

PHPUnit mock creation?

Answer

$this->createMock(Interface::class)

Question

How to verify a method was called?

Answer

$mock->expects($this->once())->method('methodName');

Question

How to return different values?

Answer

willReturnOnConsecutiveCalls() or willReturnCallback()

Revision Notes

Key Takeaways

  • 1. Stubs provide canned responses; Mocks verify interactions
  • 2. PHPUnit: createMock(), expects(), method(), willReturn()
  • 3. Constraints: $this->once(), $this->any(), $this->never(), $this->atLeastOnce()
  • 4. Mock external dependencies (DB, API, files) to keep tests fast and isolated
  • 5. Mockery is an alternative mocking library with a different syntax

Interview Tips

  • Explain the difference between mocks, stubs, and fakes
  • Give an example: mocking ProductRepository to test a service class
  • Discuss when NOT to mock (value objects, simple classes)

Cheat Sheet

Test Doubles:
  Dummy  → passed around, never used
  Stub   → provides canned answers (state)
  Mock   → verifies interactions (behavior)
  Spy    → records calls for later verification
  Fake   → simplified working implementation

PHPUnit Mocking:
  $mock = $this->createMock(Interface::class);
  $mock->expects($this->once())->method('save')
       ->with($arg)->willReturn($result);

Constraints:
  $this->once(), $this->any(), $this->never()
  $this->atLeastOnce(), $this->exactly(3)