Tutorials Logic, IN info@tutorialslogic.com

PHP Testing and Debugging: Reproduce, Assert, and Diagnose

Evidence Before Fixes

Debugging starts by reproducing one failure with known input and an observable result. Testing records the behavior that should remain true after the repair.

Prefer small pure functions and explicit dependencies because they can be exercised without a browser, database, or global state.

Debugging Sequence

  • Reproduce the smallest failing case.
  • Run php -l on the file before investigating runtime behavior.
  • Inspect exact types with var_dump() or get_debug_type() at a controlled local boundary.
  • Read the first relevant stack frame and the original exception.
  • Change one cause, rerun the reproduction, and remove temporary diagnostics.

A Testable Function

Discount Function

Discount Function
<?php
declare(strict_types=1);

function discountedTotal(float $total, int $percent): float
{
    if ($percent < 0 || $percent > 100) {
        throw new InvalidArgumentException('Invalid discount.');
    }

    return $total * (1 - $percent / 100);
}

PHPUnit Test

A PHPUnit test class extends TestCase. A test arranges input, performs one action, and asserts the observable result. Install the PHPUnit version compatible with the project runtime through Composer.

Assert Normal and Invalid Cases

Assert Normal and Invalid Cases
<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class DiscountTest extends TestCase
{
    public function testAppliesDiscount(): void
    {
        self::assertSame(80.0, discountedTotal(100.0, 20));
    }

    public function testRejectsInvalidPercentage(): void
    {
        $this->expectException(InvalidArgumentException::class);
        discountedTotal(100.0, 120);
    }
}

Test Boundaries

Choose the smallest test boundary that can prove the risk. Unit tests exercise a decision without external services, integration tests prove adapters such as PDO against the real boundary, and a small number of end-to-end tests confirm that routing, sessions, templates, and infrastructure cooperate.

Test Best Evidence Typical Cost
Unit Domain calculation or policy Fast and isolated
Integration SQL mapping, filesystem, HTTP adapter Boundary setup and cleanup
End to end Critical user workflow Slowest and broadest diagnosis

Test Doubles

Use a fake when a lightweight working implementation makes the test clear, a stub when the test needs a controlled answer, and a mock when the interaction itself is the behavior being verified. Prefer asserting the returned state or result over a long sequence of internal calls.

In-Memory Repository Fake

In-Memory Repository Fake
<?php
final class InMemoryTaskRepository implements TaskRepository
{
    /** @var list<string> */
    public array $titles = [];

    public function save(string $title): int
    {
        $this->titles[] = $title;
        return count($this->titles);
    }
}

The fake supports application tests without replacing the separate PDO integration tests that verify SQL behavior.

Useful Commands

Command Purpose
php -l src/File.php Check syntax without execution
php script.php Run a focused CLI reproduction
composer test Run the project-defined test script
vendor/bin/phpunit Run PHPUnit directly when installed

Test Quality Traps

  • Do not assert private implementation details when public behavior is enough.
  • Include boundary and failure cases, not only the happy path.
  • Keep a test independent from execution order and shared mutable state.
  • A passing test cannot prove untested security, concurrency, or integration assumptions.

Testing Strategy Check

0 of 2 checked

Q1. What should happen before changing code during debugging?

Q2. What does assertSame() compare?

Try this next

Protect One Behavior

0 of 2 completed

  1. Add tests for 0 percent, 100 percent, and a negative percentage.
  2. Pass a numeric string into a strict function and record the TypeError before fixing the caller.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.