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
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?
2. What is a data provider in PHPUnit?
3. What does createMock() do?
4. What is the difference between assertEquals and assertSame?
5. What is the @group annotation used for?
Flashcards
Question
What is PHPUnit?
Click to reveal answer
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?
Click to reveal answer
Answer
Called before each test method. Use to initialize objects, create mocks, and set up test fixtures.
Question
What is a data provider?
Click to reveal answer
Answer
A method returning arrays of test data for parameterized tests. Linked with @dataProvider annotation.
Question
What does createMock() create?
Click to reveal answer
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?
Click to reveal answer
Answer
assertEquals: loose comparison (==). assertSame: strict comparison (===). Use assertSame when type matters.
Question
How do you test exceptions?
Click to reveal answer
Answer
Use $this->expectException(ExceptionClass::class) before the code that throws, then $this->expectExceptionMessage('message').
Question
What is mock expectation matching?
Click to reveal answer
Answer
expects($this->once()) for exact count. with() for argument matching. willReturn() for return value.
Question
Where do Magento unit tests go?
Click to reveal answer
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