PHPUnit Test Structure
Basic Test Class
namespace Vendor\Catalog\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
use Vendor\Catalog\Model\PriceCalculator;
class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
public function testCalculateReturnsCorrectPrice(): void
{
$result = $this->calculator->calculate(100, 0.20);
$this->assertEquals(120.0, $result);
}
public function testCalculateWithZeroTax(): void
{
$result = $this->calculator->calculate(100, 0.0);
$this->assertEquals(100.0, $result);
}
}
Test Naming Convention
// Method name: test + Description of expected behavior
public function testCalculateWithNegativePriceThrowsException(): void { /* ... */ }
public function testCalculateAppliesTaxCorrectly(): void { /* ... */ }
public function testGetPriceReturnsBasePriceWhenNoDiscount(): void { /* ... */ }
// Or use @test annotation
/**
* @test
*/
public function discountIsAppliedBeforeTax(): void { /* ... */ }
Exception Testing
public function testNegativePriceThrowsInvalidArgumentException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Price cannot be negative');
$this->calculator->calculate(-10, 0.20);
}
// Or test that no exception is thrown
public function testValidPriceDoesNotThrow(): void
{
$this->expectNotToPerformAssertions(); // Just verify no exception
$this->calculator->calculate(100, 0.20);
}
Mock Objects in Tests
public function testProcessUsesProductRepository(): void
{
$productRepo = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class);
$productRepo->expects($this->once())
->method('get')
->with('SKU-123')
->willReturn($this->createMock(\Magento\Catalog\Api\Data\ProductInterface::class));
$processor = new ProductProcessor($productRepo);
$processor->process('SKU-123');
}
Data Providers for Parameterized Tests
What are Data Providers?
Data providers allow running the same test with different input data:
namespace Vendor\Catalog\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
class PriceCalculatorTest extends TestCase
{
/**
* @dataProvider priceCalculationProvider
*/
public function testCalculatePrice(float $price, float $tax, float $expected): void
{
$calculator = new PriceCalculator();
$result = $calculator->calculate($price, $tax);
$this->assertEquals($expected, $result, '', 0.01); // Delta for float comparison
}
public static function priceCalculationProvider(): array
{
return [
'basic tax' => [100, 0.20, 120.0],
'zero tax' => [100, 0.0, 100.0],
'high tax' => [100, 0.50, 150.0],
'small price' => [10, 0.10, 11.0],
'decimal price' => [29.99, 0.08, 32.39],
];
}
}
External Data Providers
// In a separate file
class TestDataProvider
{
public static function productData(): array
{
return [
['sku' => 'SKU-1', 'name' => 'Product 1', 'price' => 29.99],
['sku' => 'SKU-2', 'name' => 'Product 2', 'price' => 49.99],
];
}
}
// In test class
/**
* @dataProvider \Vendor\Catalog\Test\DataProvider::productData
*/
public function testProductCreation(string $sku, string $name, float $price): void
{
$product = new Product();
$product->setSku($sku)->setName($name)->setPrice($price);
$this->assertEquals($sku, $product->getSku());
$this->assertEquals($name, $product->getName());
$this->assertEquals($price, $product->getPrice());
}
DataSet Approach for Magento
// Magento uses data sets in integration tests
class ProductCreationTest extends \Magento\TestFramework\Helper\Bootstrap\AbstractTestCase
{
/**
* @magentoDataFixture Magento/Catalog/_files/product.php
*/
public function testProductExists(): void
{
$product = $this->productRepository->get('simple');
$this->assertNotNull($product->getId());
}
}
setUp, tearDown, and Test Organization
setUp and tearDown
class CartTest extends TestCase
{
private Cart $cart;
private string $tempFile;
protected function setUp(): void
{
$this->cart = new Cart();
$this->tempFile = sys_get_temp_dir() . '/cart_test_' . uniqid();
}
protected function tearDown(): void
{
// Clean up after each test
if (file_exists($this->tempFile)) {
unlink($this->tempFile);
}
}
public function testAddItem(): void
{
$this->cart->add('SKU-1', 2);
$this->assertCount(1, $this->cart->getItems());
$this->assertEquals(2, $this->cart->getItemQty('SKU-1'));
}
public function testRemoveItem(): void
{
$this->cart->add('SKU-1', 2);
$this->cart->remove('SKU-1');
$this->assertCount(0, $this->cart->getItems());
}
}
Test Organization Patterns
// Group related tests with @group annotation
/**
* @group catalog
* @group model
*/
class ProductTest extends TestCase { /* ... */ }
// Run specific group: phpunit --group catalog
// Skip tests conditionally
/**
* @requires PHP 8.1
*/
public function testNewFeature(): void { /* ... */ }
/**
* @requires extension sodium
*/
public function testEncryption(): void { /* ... */ }
// Mark tests as incomplete
public function testFeatureNotYetImplemented(): void
{
$this->markTestIncomplete('This feature is not yet implemented');
}
// Mark test as risky (no assertions)
/**
* @doesNotPerformAssertions
*/
public function testSomethingDoesNotThrow(): void
{
$this->doSomething(); // Just verify no exception
}
Best Practices
- One assertion per test (when practical)
- Test behavior, not implementation — don't test private methods
- Tests should be independent — no order dependency
- Tests should be fast — unit tests in milliseconds
- Descriptive test names —
testCalculateWithNegativePriceThrowsException
Quiz
1. What is the purpose of setUp() in PHPUnit?
2. Data providers in PHPUnit are used for:
3. A test should ideally:
Flashcards
Question
What does setUp() do?
Click to reveal answer
Answer
Runs before each test method to initialize fixtures and test objects
Question
What is a data provider?
Click to reveal answer
Answer
A method that returns arrays of data to parameterize test methods
Question
How to test exceptions in PHPUnit?
Click to reveal answer
Answer
expectException() + expectExceptionMessage() before the code that throws
Question
Test naming convention?
Click to reveal answer
Answer
test + Description of expected behavior (e.g., testCalculateWithNegativePrice)
Revision Notes
Key Takeaways
- 1. Extend TestCase, use setUp() for initialization, tearDown() for cleanup
- 2. Name tests descriptively: test + expected behavior
- 3. Data providers enable parameterized testing with different inputs
- 4. Mock objects isolate the class under test from dependencies
- 5. Tests should be fast, independent, and focused on one behavior
Interview Tips
- • Explain the difference between unit, integration, and functional tests
- • Give an example of a well-structured unit test with mocks
- • Discuss test naming conventions and why they matter
Cheat Sheet
PHPUnit Structure:
class XyzTest extends TestCase
setUp() → before each test
tearDown() → after each test
testSomething() → test method
Assertions:
assertEquals(expected, actual)
assertNotNull($var)
assertTrue($condition)
expectException(Exception::class)
dataProvider: @dataProvider methodName
public static function data(): array { return [...]; }
Run: vendor/bin/phpunit --filter testMethod