PHPUnit Setup
PHPUnit Configuration
<!-- dev/tests/unit/phpunit.xml.dist -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="bootstrap.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Magento Test Suite">
<directory>../phpunit/tests/unit</directory>
</testsuite>
<testsuite name="Module Tests">
<directory>../../../app/code/Vendor/Module/Test/Unit</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>../../../app/code/Vendor/Module</directory>
</include>
</coverage>
</phpunit>
Running Tests
# Run all unit tests
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist
# Run specific test suite
vendor/bin/phpunit --testsuite "Module Tests"
# Run specific test class
vendor/bin/phpunit app/code/Vendor/Module/Test/Unit/Model/ProductTest.php
# Generate coverage report
vendor/bin/phpunit --coverage-html dev/tests/unit/coverage app/code/Vendor/Module/Test/Unit/
Bootstrap File
// dev/tests/unit/bootstrap.php
require_once BP . '/vendor/autoload.php';
use Magento\Framework\App\Bootstrap;
$bootstrap = Bootstrap::create(BP, $_SERVER);
$objectManager = $bootstrap->getObjectManager();
Key Points
- Unit tests isolate individual classes
- Tests should be fast (milliseconds)
- Mock all external dependencies
- Aim for high code coverage (80%+)
Writing Test Cases
Basic Test Structure
namespace Vendor\Module\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
use Vendor\Module\Model\Product;
class ProductTest extends TestCase
{
private $product;
protected function setUp(): void
{
$this->product = new Product();
}
public function testGetName()
{
$this->product->setName('Test Product');
$this->assertEquals('Test Product', $this->product->getName());
}
public function testGetPrice()
{
$this->product->setPrice(99.99);
$this->assertEquals(99.99, $this->product->getPrice());
}
public function testPriceCannotBeNegative()
{
$this->expectException(\Exception::class);
$this->product->setPrice(-10);
}
}
Assertion Methods
public function testAssertions()
{
// Equality
$this->assertEquals(10, $value);
$this->assertEqualsCanonicalizing([3, 1, 2], [1, 2, 3]);
// Identity
$this->assertSame(10, $value); // Strict === comparison
// Type
$this->assertIsString($value);
$this->assertIsArray($value);
// Array
$this->assertArrayHasKey('key', $array);
$this->assertContains('value', $array);
// String
$this->assertStringContainsString('needle', $haystack);
$this->assertStringStartsWith('prefix', $string);
// Null
$this->assertNull($value);
$this->assertNotNull($value);
// Exception
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Error message');
}
Key Points
- Test methods start with 'test'
- Use setUp() for common initialization
- Use tearDown() for cleanup
- One assertion per test method (ideally)
Mocks and Stubs
Creating Mocks
use PHPUnit\Framework\MockObject\MockObject;
class ProductRepositoryTest extends TestCase
{
private $repository;
private $connectionMock;
protected function setUp(): void
{
$this->connectionMock = $this->getMockBuilder(
\Magento\Framework\DB\Adapter\PDO\Adapter::class
)->disableOriginalConstructor()
->getMock();
$this->repository = new ProductRepository($this->connectionMock);
}
public function testFindProduct()
{
$expectedProduct = ['entity_id' => 1, 'sku' => 'TEST-001'];
$this->connectionMock->expects($this->once())
->method('fetchRow')
->with(
$this->stringContains('catalog_product_entity'),
$this->equalTo(['entity_id' => 1])
)
->willReturn($expectedProduct);
$result = $this->repository->findById(1);
$this->assertEquals($expectedProduct, $result);
}
}
Mock Behaviors
// Different return values for multiple calls
$mock->method('getValue')
->willReturnOnConsecutiveCalls('first', 'second', 'third');
// Return based on arguments
$mock->method('validate')
->willReturnMap([
['valid@email.com', true],
['invalid-email', false],
['', false],
]);
// Throw exception
$mock->method('save')
->willThrowException(new \Exception('Save failed'));
// Callback
$mock->method('process')
->willReturnCallback(function($data) {
return strtoupper($data);
});
Key Points
- Mock external dependencies, not the class under test
- Use expects() to verify method calls
- Use with() to specify expected arguments
- Use willReturn() to define return values
Data Providers
Data Provider Usage
class PriceCalculatorTest extends TestCase
{
private $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
/**
* @dataProvider priceCalculationProvider
*/
public function testCalculatePrice($price, $quantity, $discount, $expected)
{
$result = $this->calculator->calculate($price, $quantity, $discount);
$this->assertEquals($expected, $result, '', 0.01);
}
public function priceCalculationProvider(): array
{
return [
'basic calculation' => [100, 2, 0, 200],
'with discount' => [100, 2, 10, 180],
'zero quantity' => [100, 0, 0, 0],
'decimal price' => [19.99, 3, 5, 56.97],
'high discount' => [100, 1, 50, 50],
];
}
}
External Data Providers
class ValidationTest extends TestCase
{
/**
* @dataProvider externalDataProvider
*/
public function testValidation($input, $expected)
{
$this->assertEquals($expected, $this->validate($input));
}
public static function externalDataProvider(): array
{
return require __DIR__ . '/data/validation-cases.php';
}
}
// data/validation-cases.php
return [
'valid email' => ['test@example.com', true],
'invalid email' => ['not-an-email', false],
'empty string' => ['', false],
];
Key Points
- Data providers must be public static methods
- Return array of arrays with test data
- Name data cases for clear test output
- Use external files for large datasets
Practice Problems
0 / 1 solved
Write Unit Tests
Write unit tests for a price calculator class that applies discounts and taxes.
Solution
<?php
namespace Vendor\Module\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
use Vendor\Module\Model\PriceCalculator;
class PriceCalculatorTest extends TestCase
{
private $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
/** @dataProvider priceProvider */
public function testCalculate($price, $tax, $discount, $expected)
{
$result = $this->calculator->calculate($price, $tax, $discount);
$this->assertEquals($expected, $result, '', 0.01);
}
public function priceProvider(): array
{
return [
'no discount' => [100, 20, 0, 120],
'with discount' => [100, 20, 10, 108],
'zero price' => [0, 20, 0, 0],
'high tax' => [100, 50, 0, 150],
];
}
} Quiz
1. What makes a good unit test?
2. What is the purpose of mocking?
3. When should setUp() be used?
4. What is a data provider?
Flashcards
Question
Unit test characteristics?
Click to reveal answer
Answer
Fast, isolated, repeatable, single class focus
Question
Mock purpose?
Click to reveal answer
Answer
Replace external dependencies for isolation
Question
setUp() runs when?
Click to reveal answer
Answer
Before each test method
Question
Data provider method?
Click to reveal answer
Answer
Public static method returning array of test data
Revision Notes
Key Takeaways
- 1. Unit tests isolate single classes with mocked dependencies
- 2. Use setUp() for initialization, tearDown() for cleanup
- 3. Mocks verify interactions and replace external dependencies
- 4. Data providers enable parameterized testing
Interview Tips
- • Explain the difference between mocks and stubs
- • Discuss test-driven development (TDD) benefits
- • Know PHPUnit assertion methods
Cheat Sheet
Magento Unit Tests
- Run: vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist
- Mock: getMock(), createMock()
- Assert: assertEquals, assertSame, assertNull
- Data: @dataProvider annotation