PHPUnit Basics#
PHPUnit is the standard testing framework for PHP.
Setup#
composer require --dev phpunit/phpunit<!-- phpunit.xml -->
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src/</directory>
</include>
</source>
</phpunit>Writing Tests#
<?php
// tests/Calculator/CalculatorTest.php
namespace App\Tests\Calculator;
use App\Calculator\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAddPositiveNumbers(): void
{
$result = $this->calculator->add(2, 3);
$this->assertSame(5, $result);
}
public function testDivideByZeroThrowsException(): void
{
$this->expectException(\DivisionByZeroError::class);
$this->calculator->divide(10, 0);
}
/**
* @dataProvider additionProvider
*/
public function testAddWithDataProvider(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $this->calculator->add($a, $b));
}
public static function additionProvider(): array
{
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'zero' => [0, 0, 0],
'mixed signs' => [-5, 3, -2],
];
}
}Running Tests#
vendor/bin/phpunit # All tests
vendor/bin/phpunit tests/Calculator/ # Specific directory
vendor/bin/phpunit --filter=testAdd # Specific test method
vendor/bin/phpunit --coverage-html=coverage/ # With code coverage reportAssertions#
| Assertion | Purpose |
|---|---|
assertSame($expected, $actual) | Strict equality (===) |
assertEquals($expected, $actual) | Loose equality (==) |
assertTrue($value) | Assert truthy |
assertFalse($value) | Assert falsy |
assertNull($value) | Assert null |
assertCount($n, $array) | Array/Countable has exactly N items |
assertContains($needle, $haystack) | Array contains value |
assertArrayHasKey($key, $arr) | Array has specific key |
assertInstanceOf(Class::class, $obj) | Object is instance of class |
assertStringContainsString($sub, $str) | String contains substring |
assertMatchesRegularExpression($re, $str) | Regex match |
Prefer assertSame over assertEquals — strict comparison catches type bugs.
Mocking#
Mock external dependencies to test in isolation:
<?php
use PHPUnit\Framework\TestCase;
class OrderServiceTest extends TestCase
{
public function testPlaceOrderSendsNotification(): void
{
// Create mock
$notifier = $this->createMock(NotifierInterface::class);
// Set expectation: notify() must be called exactly once with any Order
$notifier->expects($this->once())
->method('notify')
->with($this->isInstanceOf(Order::class));
// Inject mock
$service = new OrderService($notifier);
$service->placeOrder(new Order(amount: 99.99));
}
public function testGetUserReturnsFromRepository(): void
{
$user = new User(id: 1, name: 'Alice');
$repo = $this->createMock(UserRepository::class);
$repo->method('find')
->with(1)
->willReturn($user);
$service = new UserService($repo);
$result = $service->getUser(1);
$this->assertSame($user, $result);
}
}Test Organization#
tests/
├── Unit/ # Fast, isolated, no I/O
│ ├── Service/
│ │ └── OrderServiceTest.php
│ └── Calculator/
│ └── CalculatorTest.php
├── Integration/ # Tests with real database/APIs
│ └── Repository/
│ └── UserRepositoryTest.php
└── Functional/ # Full request/response cycle
└── Api/
└── OrderEndpointTest.phpKey Takeaways#
- One test class per production class, mirroring the
src/structure - Use
assertSame(strict) overassertEquals(loose) by default - Data providers eliminate repetitive test methods — test many inputs in one method
- Mock interfaces, not concrete classes — this enforces proper dependency inversion
- Test behavior, not implementation — verify what a method does, not how it does it
setUp()for shared setup,tearDown()for cleanup — runs before/after each test