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?
2. PHPUnit's expects($this->once()) means:
3. When should you use a mock vs a real object?
Flashcards
Question
Stub vs Mock?
Click to reveal answer
Answer
Stub = provides canned data. Mock = verifies method calls.
Question
PHPUnit mock creation?
Click to reveal answer
Answer
$this->createMock(Interface::class)
Question
How to verify a method was called?
Click to reveal answer
Answer
$mock->expects($this->once())->method('methodName');
Question
How to return different values?
Click to reveal answer
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)