Testing and Debugging PHP Applications
I don't care much about hitting some coverage percentage. I care that the important stuff (the money math, the permission checks, the business rules) gets tested every time, so I find out about breakage before a customer does.
Choose the right test level
| Level | Best for | Tradeoff |
|---|---|---|
| Unit | Business rules and value objects | Fast, but does not prove integration. |
| Integration | Repositories, SQL, filesystem, and APIs | More realistic, slower, needs controlled dependencies. |
| HTTP/feature | Routes, authentication, validation, and response behavior | Covers more layers but failures are less isolated. |
| End-to-end | Critical user journeys | Most realistic and most expensive to maintain. |
A unit test for a business rule
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class InvoiceLineTest extends TestCase
{
public function testCalculatesTotalInCents(): void
{
$line = new InvoiceLine(
description: 'Charter service',
quantity: 3,
unitPriceCents: 19_500
);
self::assertSame(58_500, $line->totalCents());
}
public function testRejectsZeroQuantity(): void
{
$this->expectException(InvalidArgumentException::class);
new InvoiceLine('Invalid line', 0, 1000);
}
}
Test SQL against a real database engine
SQLite is handy for quick tests, but it isn't MySQL. If a query depends on MySQL's types, collations, locking, JSON functions, or generated columns, test it against a real (throwaway) MySQL database. Otherwise your tests pass and production doesn't.
- Run migrations before the suite.
- Wrap each test in a transaction when possible.
- Seed only the records the test needs.
- Assert database state and externally visible behavior.
- Keep production data out of test environments.
Useful debugging tools
php -l src/Service/CreateInvoice.php php -d display_errors=1 -d error_reporting=E_ALL script.php composer test vendor/bin/phpunit --filter InvoiceLineTest # Inspect an HTTP response curl -i https://example.test/api/health # Follow PHP-FPM or application logs journalctl -u php-fpm -f tail -f storage/logs/application.log
Debug the data flow
Pick one value and follow it the whole way: request input, cleanup, permission check, business rules, SQL, and output. A lot of bugs turn out to be a value quietly changing type or meaning somewhere between layers. An empty string becomes null, a string ID gets compared to an int, that kind of thing.
Handling a production bug
- Reduce impact first: disable the feature, roll back, or apply a safe configuration change.
- Preserve evidence before restarting or clearing logs.
- Identify the first incorrect state, not only the final exception.
- Reproduce in a controlled environment.
- Add a regression test that fails for the original defect.
- Deploy the smallest safe correction.
- Review why existing checks and alerts did not catch it sooner.
