Learn PHP unit testing using PHPUnit. Discover testing strategies, mocks, coverage tools, and CI integration for properly working PHP codebases.
Unit tests help developers expose bugs early and keep code quality intact over time. This article discusses PHP unit testing through PHPUnit, the best‑known PHP testing framework. You will learn how to write real tests, organise your test suite, create mocks for dependencies, and add CI pipelines such as GitHub Actions.
Installing PHPUnit
Testing a unit guarantees that every aspect of a code element behaves as expected.
Composer install command
composer require --dev phpunit/phpunit
Initialise a test suite
./vendor/bin/phpunit --init
This generates the phpunit.xml
configuration file and the tests/
directory.
You can run all tests with:
./vendor/bin/phpunit
Writing Your First PHPUnit Test
Let’s say you have a simple class:
class MathEngine {
public function sum($num1, $num2) {
return $num1 + $num2;
}
}
Now create a test:
// tests/MathEngineTest.php
use PHPUnit\Framework\TestCase;
class MathEngineTest extends TestCase {
public function testSumOperation() {
$engine = new MathEngine();
$this->assertEquals(4, $engine->sum(2, 2));
}
}
Use assertions like assertEquals
, assertTrue
, assertFalse
, assertInstanceOf
, etc.
PHPUnit Test Structure and Best Practices
Arrange–Act–Assert Pattern
- Arrange: Set up objects and dependencies
- Act: Call the method you’re testing
- Assert: Verify the result
public function testZeroDivisionRaisesError() {
$engine = new MathEngine();
$this->expectException(DivisionByZeroError::class);
$engine->divide(10, 0);
}
Group Related Tests into Classes
Group tests logically by class or module, e.g.:
- UserTest
- OrderRepositoryTest
- NotificationServiceTest
Keep each test class focused on one unit.
Mocking and Stubs in PHPUnit
When testing units in isolation, you often need to mock dependencies that would otherwise trigger side effects (DB, API, mail, etc.).
Creating a mock
$fakeNotifier = $this->createMock(Notifier::class);
$fakeNotifier->expects($this->once())
->method('dispatch')
->with('member@example.com');
Inject this mock into the class being tested:
$notifier = new UserNotifier($mockMailer);
$notifier->sendWelcomeEmail('user@example.com');
Code Coverage with PHPUnit
Install Xdebug or PCOV and run:
./vendor/bin/phpunit --coverage-html coverage/
This generates a visual report in the coverage/
directory showing tested vs. untested lines.
Note: 100 % coverage doesn’t guarantee bug‑free code, but low coverage usually signals trouble.
Test‑Driven Development (TDD) in PHP
TDD is the practice of writing a test before writing the code it tests. The cycle is:
- Red: Write a failing test
- Green: Write minimal code
- Refactor: Clean up both the test and the code
TDD keeps your design minimal and focused on actual needs.
public function testLinkFormatter() {
$formatter = new UrlSlugMaker();
$this->assertEquals('hello-world', $formatter->makeSlug('Hello World'));
}
class UrlSlugMaker {
public function makeSlug(string $text): string {
return strtolower(str_replace(' ', '-', $text));
}
}
Testing Exceptions and Edge Cases
Example: testing exceptions
$this->expectException(InvalidArgumentException::class);
$parser->parse(null);
You can also test:
- Invalid input
- Boundary values
- Nulls and empty arrays
- Optional parameters
Organising Your Test Suite
Best practices
- Use PSR‑4 autoloading for tests
- Mirror your
src/
structure undertests/
- Name test files
*Test.php
- Keep one class per file
Example structure
/src
/Service
UserService.php
/tests
/Service
UserServiceTest.php
Integrating PHPUnit with GitHub Actions
You can automatically run your PHP unit tests on every push or PR using GitHub Actions.
Example workflow .github/workflows/php.yml
name: Run PHPUnit Tests
on: [push, pull_request]
jobs:
Test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xdebug
tools: composer
- run: composer install --no-progress
- run: vendor/bin/phpunit
This CI setup ensures tests run automatically, reducing the risk of bugs being merged.
PHPUnit in Laravel and Symfony
Laravel Testing
Laravel uses PHPUnit by default and adds helpers:
$this->get('/api/user')->assertStatus(200);
You can use factories, database transactions, and Laravel’s artisan test
runner.
Symfony Testing
Symfony also uses PHPUnit, with built‑in integration and testing environments:
$client = static::createClient();
$client->request('GET', '/blog');
$this->assertResponseIsSuccessful();
Use:
- KernelTestCase
- ApiTestCase
- WebTestCase
PHP unit testing is one practice every developer should embrace. With the PHPUnit framework, you can test business logic and prevent regressions across the code base. So start slow, test a lot, and let your tests guide your design.
Common Pitfalls to Avoid
- Testing private methods: always test public behaviour.
- Too many assertions per test: test only what matters.
- Not resetting state: use
setUp()
andtearDown()
. - Skipping mocks: never call real APIs or DBs in unit tests.
- Mixing Unit and Integration tests: keep responsibilities clear.