NightFox

NightFox

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

LevelBest forTradeoff
UnitBusiness rules and value objectsFast, but does not prove integration.
IntegrationRepositories, SQL, filesystem, and APIsMore realistic, slower, needs controlled dependencies.
HTTP/featureRoutes, authentication, validation, and response behaviorCovers more layers but failures are less isolated.
End-to-endCritical user journeysMost 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

  1. Reduce impact first: disable the feature, roll back, or apply a safe configuration change.
  2. Preserve evidence before restarting or clearing logs.
  3. Identify the first incorrect state, not only the final exception.
  4. Reproduce in a controlled environment.
  5. Add a regression test that fails for the original defect.
  6. Deploy the smallest safe correction.
  7. Review why existing checks and alerts did not catch it sooner.

Last Modified: October 12, 2026, 11:46 am