Course outline

PHP / Intermediate: objects, persistence, and tests / Lesson 12

Testing PHP contracts with isolated evidence

Build deterministic checks for values, errors, output, and state. Learn what a tiny CLI harness can prove and where HTTP, database, or application integration evidence remains necessary.

About 4 min readContent reviewed
On this page

Lesson goals

  • Use independent expected values and strict assertions.
  • Control fixtures, time, and failure injection.
  • Distinguish focused tests from integration and coverage metrics.

A test needs a reasoned expected result

A test arranges state, performs an action, and compares an observation with an independent oracle. Reusing the production calculator to compute expected repeats its bug rather than detecting it. For a price rule, record reviewed integer-cent examples and boundary assumptions. Assert type as well as value when that matters, since loose equality can hide an unintended string result. Do not rely on PHP assert being enabled for a teaching harness; configuration can disable assertion evaluation. An explicit check that throws is a predictable local signal without installing a framework or loading application services.

php Explicit strict assertions independent of assert settings
<?php
declare(strict_types=1);
function same(mixed $actual, mixed $expected): void {
    if ($actual !== $expected) { throw new RuntimeException("assertion failed"); }
}
function total(int $price, int $count): int { return $price * $count; }
same(total(125, 4), 500);
same(total(0, 4), 0);
echo "2 checks passed", PHP_EOL;
Expected output
2 checks passed

The expected constants are derived separately. A wrong numeric type or value fails the check.

Give each case its own starting state

Fresh arrays, value objects, temporary directories, and in-memory databases prevent cases from inheriting accidental state. Keep setup and cleanup paired even if the action throws. A test should not need another test to create an account or change the clock first. Inject a clock or random source when the rule depends on them so boundary values are controllable. Never use configured application databases, real session stores, or private records as convenient fixtures. A test framework can manage lifecycle and reporting, but it cannot repair a vague oracle or choose meaningful synthetic data for the domain.

Test a specific failure and its forbidden effects

A negative test should verify the intended error category and unchanged relevant state, not accept any exception. An unrelated class-not-found error could otherwise satisfy a validation test. For asynchronous or external adapters, use controlled local substitutes and preserve a separate integration check of the real contract. The example catches only the expected type classification and inspects a counter that must remain unchanged after rejection. It demonstrates an ordinary synchronous PHP contract, not proof of a remote payment or transaction. Keep the harmful outcome each assertion detects visible in the test name and explanation.

php Error classification and absence of mutation
<?php
declare(strict_types=1);
$state = ["reserved" => 0];
function reserve(array &$state, int $count): void {
    if ($count < 1) { throw new InvalidArgumentException("positive count required"); }
    $state["reserved"] += $count;
}
$rejected = false;
try { reserve($state, 0); } catch (InvalidArgumentException $error) { $rejected = true; }
if (!$rejected || $state["reserved"] !== 0) { throw new RuntimeException("rejection contract failed"); }
echo "rejected; reserved=", $state["reserved"], PHP_EOL;
Expected output
rejected; reserved=0

A rejected quantity cannot increment the counter. A different uncaught defect would fail rather than count as correct rejection.

Choose evidence for the actual question

Pure unit cases explain arithmetic and decisions cheaply. PDO integration checks show schema and transaction interaction on a specified driver. An HTTP adapter check observes actual status, headers, cookies, and body, which a CLI model cannot establish. A browser journey investigates rendered usability and correction. Keep a few meaningful composed cases instead of repeating every arithmetic combination through a browser. When a defect is fixed, rerun its original reproducer for confirmation and select related regression cases from changed dependencies. State which level, host, and assumptions each result covers so a local pass is not inflated into a product-wide claim.

Coverage measures execution, not confidence by itself

Statement or branch coverage describes measured code execution in a defined run. It does not detect omitted requirements or prove assertions would notice a wrong result. A suite that executes every line but checks only that nothing crashed may have weak evidence. Review difficult boundaries, errors, and business side effects alongside metrics. Preserve the first failure when a test is intermittent, compare setup and environment, and fix state leakage instead of repeatedly rerunning until green. Report planned, executed, failed, and blocked cases with their risks rather than presenting a percentage as the fraction of the product known correct.

Common errors and fixes

A test relies on assert configuration

Why it happens
Assertion evaluation may be disabled in the runtime.
How to fix it
Use an explicit throwing check or a properly configured test runner.
Prevent it next time
Verify a deliberately wrong local result is detected.

Any exception makes a negative test pass

Why it happens
The failure oracle is too broad.
How to fix it
Check the intended type and forbidden state effects.
Prevent it next time
Keep a healthy positive control in the same fixture.

Tests share a mutable starting record

Why it happens
Fixture ownership and reset are implicit.
How to fix it
Create independent state and reliable cleanup.
Prevent it next time
Run the minimal suspected pair in both orders when diagnosing.

Check your understanding

1. Why is expected=total(...) a poor oracle for testing total(...)?

Show worked solution

It repeats the same implementation, so a shared arithmetic defect can agree with itself. Use reviewed constants such as 125 times four equals 500, plus meaningful zero and boundary cases. Test rejected types or ranges under the documented contract rather than assuming execution coverage proves correctness.

2. Which additional evidence is needed after a CLI response-model test passes?

Show worked solution

Use an isolated HTTP adapter test to inspect real status, content type, headers, cookies, and body. If user interaction matters, add a browser task including correction and keyboard behavior. Keep the CLI result as focused construction evidence without claiming those unobserved integration properties.

Key takeaways

  • Use independent oracles and owned fixtures.
  • Confidence follows meaningful observations and explicit limits.

Official references