Introduction
PHPUnit is lesson 113 in the PHP Advanced pathway. PHPUnit develops evidence through testing, analysis, models, counterexamples and explicit limits of each verification technique.
This lesson develops a precise mental model, executable PHP examples, boundary and security analysis, verification guidance, practice questions and a hands-on project. Record the exact PHP branch, SAPI, extensions and dependencies used for your work.
Explanation
Learning outcomes
After completing PHPUnit, you should be able to explain the concept, identify the governing PHP language, extension, protocol, framework or runtime contract, predict a small example, implement a defensible solution and diagnose common failures. You should state accepted inputs, returned values, effects, exceptions, resource ownership, cleanup, compatibility and security boundaries.
At the Advanced level, competence means more than recalling an API spelling. Connect PHPUnit to PHP values, arrays, objects, calls, requests, responses, persistence and deployment. Advanced and research work also needs concurrency, performance, failure timing, implementation and reproducibility arguments. One successful request proves one case; dependable software needs a contract and diverse evidence.
Core idea and scope
PHPUnit develops evidence through testing, analysis, models, counterexamples and explicit limits of each verification technique. Advanced PHP systems combine framework lifecycle, databases, caches, identity, queues, processes, network protocols, observability and deployment. Failure timing and long-running state matter as much as normal request behavior. Framework, server, database, cache and runtime APIs evolve separately. Record PHP, framework, extension and infrastructure versions with every operational claim.
The core vocabulary includes dependency direction, idempotency, backpressure, cache coherence, observability, service objective. Define each term in a concrete program. The PHP manual distinguishes language behavior from extension interfaces. Composer packages, PHP-FIG specifications, databases, web servers, browsers and frameworks define additional contracts. Do not promote one observation from a development machine into a universal guarantee.
Scope the topic with four questions. What preconditions and input forms are accepted? What value, state change, I/O, message, response or artifact is produced? What can fail and how is failure represented? What invariant must hold before and after the operation? These questions convert a loose feature into a reviewable interface.
Use a minimal example first, then add one concern at a time. Mixing routing, authentication, persistence, queues, templates, caches and deployment in the first experiment hides the cause of errors. A small example is valuable when it exposes the exact transition being studied.
Execution and system model
Define component states, owners, transaction and message boundaries, synchronization, retries, idempotency, invalidation, trust boundaries, service objectives and shutdown order. Apply this model to PHPUnit. Draw the relevant variables, objects, services, resources or distributed participants and label every state-changing edge. Mark the point where request data becomes validated domain data and where a component assumes ownership of a file, connection, transaction, lock, worker, job or response.
PHP variables hold values represented by runtime structures; arrays are ordered maps; objects have class-defined state and behavior; references create deliberate aliasing semantics. Function calls bind arguments to parameters, may coerce or reject types according to declarations and strictness, and can return or throw. Request-based SAPIs often reset application state per request, while persistent workers retain process memory and expose stale-state risks.
Separate language rules from environment behavior. php.ini changes error reporting, limits, sessions, uploads, extensions, OPcache and more. The SAPI determines CLI, FPM or server integration. The web server controls forwarding and limits. Databases define isolation and locking. Frameworks define containers and middleware. State every relevant layer.
For external data, validate type, length, range, encoding, shape, authorization and resource cost before effect. Superglobals, JSON, XML, uploaded files, SQL rows, cache entries, messages, environment variables and command arguments remain untrusted after parsing. Sanitization is not a substitute for validation or destination-specific encoding.
Implementation method
Hide infrastructure behind ports, keep domain invariants central, make retries idempotent, bound queues and inputs, define cache invalidation, use standard security protocols and retain a simple reference before optimization. Start with a one-sentence contract for PHPUnit. Name input, output, effects, failure, ownership and cleanup. For HTTP include method, route, headers, body, status, authentication, authorization and idempotency. For persistence include transaction boundaries and concurrency. For background work include acknowledgement, retry, deduplication and poison-message policy.
Choose representations that make invalid states difficult. Use value objects, enums, readonly properties, typed DTOs, dedicated exceptions and interfaces when they clarify the contract. Convert at boundaries and keep internal code working with validated values. Avoid arrays whose undocumented string keys silently become an internal protocol.
Implement a small normal path and make it observable. Return values instead of printing from domain logic. Inject a clock, random source, database, transport, filesystem or cache boundary instead of reading hidden global state. Structured logs and metrics should identify safe operation context without exposing secrets or personal data.
Add failure behavior deliberately. Reject invalid input early, preserve exception context, roll back transactions, release resources and avoid publishing partial state. Catch only failures the current layer can recover from or translate meaningfully. A broad Throwable catch belongs at a deliberate process, request or worker boundary, not around every function.
Refactor after tests protect behavior. Extract cohesive services, clarify names, remove duplication, narrow public surfaces and document version or runtime assumptions. Static analysis and formatting improve consistency, but they complement behavioral and integration evidence.
Worked PHP example
Read the example before running it. Predict values, references, output, exceptions, external effects and cleanup. Identify dependencies on a PHP version, extension, SAPI, operating system, service or untrusted value.
<?php
declare(strict_types=1);
function normalizeUsername(string $value): string
{
$value = mb_strtolower(trim($value), 'UTF-8');
if ($value === '' || mb_strlen($value, 'UTF-8') > 64) throw new InvalidArgumentException('invalid length');
if (!preg_match('/\A[\p{L}\p{N}._-]+\z/u', $value)) throw new InvalidArgumentException('invalid character');
return $value;
}
assert(normalizeUsername(' Ada.Lovelace ') === 'ada.lovelace');
foreach (['', '!', str_repeat('x', 65)] as $invalid) {
try { normalizeUsername($invalid); assert(false); } catch (InvalidArgumentException) {}
}
Trace it from entry to completion. Identify mutable state and aliases. State loop or retry termination. For files, transactions, tasks, workers, locks and temporary artifacts, mark acquisition and release. For annotations and declarations, distinguish developer tooling from runtime enforcement.
Change a central boundary: empty input, one value, duplicate data, invalid type, maximum allowed size, Unicode, timeout, unavailable service, permission denial, repeated request, concurrent update, unsupported version or missing extension. Write the expected result, exception, response status, persistent state and cleanup before executing.
Make one deliberately broken copy: use loose comparison for a token, interpolate SQL, render unescaped HTML, trust an upload name, omit a transaction rollback, retry a side effect, leak request state in a worker or cache without invalidation. Use a test, analyzer, debugger, trace, profile, protocol capture or invariant assertion to expose it. Remove the unsafe copy after retaining a regression.
Independent verification
The second artifact is a smaller reference or verification path. Compare its assumptions with the primary implementation.
<?php
declare(strict_types=1);
function verifyEquivalent(callable $reference, callable $candidate, array $cases): void
{
foreach ($cases as $case) {
$expected = $reference($case);
$start = hrtime(true);
$actual = $candidate($case);
$elapsed = hrtime(true) - $start;
assert($actual === $expected);
assert($elapsed >= 0);
}
}
verifyEquivalent('array_sum', static fn(array $v): int|float => array_sum($v), [
[], [0], [1, -1, 2], range(1, 100),
]);
Independent evidence should fail differently. A hand reference can check an optimized implementation. Static analysis can find flows not covered at runtime. A temporary database can verify transactions. Protocol tests can inspect messages. Differential execution can compare PHP revisions, runtime modes or libraries. Source and opcode inspection can expose behavior hidden by an API.
Record negative evidence. Timeouts, skips, unsupported platforms, analyzer unknowns, warnings, flaky outcomes and failed replications are not passes. Preserve them as distinct statuses and investigate whether they narrow the supported contract.
Failure modes and security
1. Applying a pattern without its motivating problem. During a PHPUnit review, name the violated contract, minimize the failure, repair it and retain a regression test.
2. Leaking request state into a persistent worker. During a PHPUnit review, name the violated contract, minimize the failure, repair it and retain a regression test.
3. Retrying non-idempotent effects. During a PHPUnit review, name the violated contract, minimize the failure, repair it and retain a regression test.
4. Caching without an invalidation policy. During a PHPUnit review, name the violated contract, minimize the failure, repair it and retain a regression test.
5. Optimizing from an unrepresentative benchmark. During a PHPUnit review, name the violated contract, minimize the failure, repair it and retain a regression test.
Classify failures by layer: parsing, type or value, application invariant, external resource, protocol, concurrency, security, deployment or performance. Begin with the exception type, message and innermost relevant frame. Inspect actual values, configuration and state. Change one factor per diagnostic experiment.
Test cleanup directly. Fail after each acquisition and confirm that files close, transactions roll back, temporary files disappear, locks release, jobs reach a defined state, workers remain healthy and partial responses or events are not published. A finally block is useful only when it knows which stages completed.
Treat external values as hostile. Use prepared statements, encode output for HTML, attribute, URL, JavaScript or JSON context, validate CSRF tokens, rotate sessions after authentication, set Secure, HttpOnly and SameSite cookie attributes, restrict uploaded types and destinations, and avoid shell invocation. Authorization must be checked at the protected effect.
Protect secrets outside source control. Use supported password hashing, secure random values, TLS verification, least privilege and managed key rotation. Do not implement cryptographic protocols from primitives unless the project is specifically qualified to do so.
Architecture and maintainability
Readable PHP exposes dependencies and lifecycle. Keep controllers thin, domain rules independent of frameworks, persistence behind explicit boundaries and templates free of business decisions. Prefer composition when inheritance does not represent a stable substitutable relationship. Avoid a service locator disguised as dependency injection.
Modules and packages need directional dependencies. Composer metadata, minimum PHP version, extensions, public API, migrations and configuration are part of design. A package should not perform surprising I/O merely because it was autoloaded.
Document accepted forms, units, ordering, mutability, ownership, blocking, thread or async safety, idempotency, complexity, precision, security requirements, exceptions and compatibility. Examples help, but an example cannot state every constraint.
For frameworks, distinguish application code from framework adapters. Versioned routes and messages deserve schemas. ORM entities are not automatically domain models. Service containers should assemble dependencies near the boundary rather than hide them throughout the code.
Testing and diagnostics
Combine unit, integration, contract, security, property, concurrency, load and failure-injection tests with profiling and production-like telemetry. Tests should describe behavior through public boundaries. Assert values, responses, database state, messages, files, metrics and exceptions rather than incidental private call sequences.
Create a boundary table for PHPUnit: typical, empty, singleton, minimum, maximum, just outside, malformed, duplicate, repeated, adversarial and dependency-failure cases. Add Unicode, time zones, concurrent updates, cancellation, large input or long-running worker state when relevant. Keep a regression for every defect.
Use fixtures to own setup and cleanup. Keep tests order-independent. Control clocks, randomness and external systems through explicit interfaces. Use real serializers, databases, protocol clients and framework kernels in integration tests where their behavior is a risk; mocks cannot prove integration.
Static analysis, coding standards, unit tests, property tests, integration tests, security scanning, mutation testing, profiling and production telemetry answer different questions. State the coverage and blind spots of each tool selected for PHPUnit.
When debugging, minimize the request or input. Inspect types, strictness, configuration, headers, response status, query parameters, SQL bindings, transaction state, generated messages, loaded classes, OPcache state and worker lifetime as appropriate. A minimal reproducer becomes a durable test.
Performance and resources
Choose algorithms and data representations before micro-optimizing syntax. Establish a correctness oracle and representative workload. Record PHP and Zend versions, SAPI, extensions, php.ini, OPcache/JIT settings, framework, dependencies, server, database, operating system, hardware, warm-up, trial order and all observations.
Distinguish latency, throughput, memory, CPU, I/O, database, network, startup and cost. A gain in one can worsen another. Profile to locate dominant work, then test a mechanism. A tiny timing loop teaches mechanics but does not justify a production conclusion without calibration and uncertainty.
Common improvements reduce total work, batch I/O, avoid N+1 queries, add justified indexes, cache with correct keys and invalidation, optimize autoloading and OPcache, stream large results, reuse safe persistent connections and move slow jobs off the request path. Keep a clear reference and equivalence tests.
Bound body sizes, uploads, query results, pagination, recursion, queues, concurrency, retries, response buffering, logs, temporary storage and cache growth. Backpressure and admission control are correctness features when arrivals can exceed capacity.
Practice questions
1. Define PHPUnit and identify its PHP language, extension, protocol, framework or runtime contract.
2. List involved values, objects, services and resources and draw their relationships.
3. Write input, output, effects, exceptions, ownership, cleanup and compatibility rules.
4. Predict the worked example before executing it.
5. Separate portable PHP behavior from a Zend Engine, SAPI or platform detail.
6. Add empty, typical, boundary, malformed and adversarial tests.
7. Identify a coercion, comparison, reference or mutability risk.
8. Force the earliest dependency failure and prove cleanup.
9. Replace hidden time, randomness, globals, filesystem, network or database access with a boundary.
10. Add precise types and explain what runtime validation remains necessary.
11. Create a broken variant and choose the best diagnostic tool.
12. Write a property that holds across generated inputs.
13. Threat-model the most dangerous external value.
14. Design a real integration or protocol test.
15. Define a performance metric and representative workload.
16. Record PHP, SAPI, extensions, dependencies, configuration, platform and raw outcomes.
17. Review compatibility across supported branches and framework releases.
18. Refactor for clarity while proving unchanged behavior.
Hands-on project
Build a small package or application centered on PHPUnit. Write a README with supported PHP versions, extensions, installation, interface, inputs, outputs, effects, exceptions, security, resource ownership and examples. Use Composer autoloading for a multi-class project and keep public web files separate from private source and configuration.
Implement a clear reference first. Add validated boundaries, typed interfaces, dedicated exceptions, deterministic cleanup and structured results. Separate domain logic from HTTP, CLI and persistence adapters. Define transactions, authorization, idempotency and long-running worker reset where relevant.
Create at least twelve tests: normal, empty, singleton, lower and upper boundary, malformed, repeated, dependency failure, cleanup, security rejection, compatibility and regression. Add integration tests for real extensions or protocols and run static analysis appropriate to the level.
Add useful observability without secrets. Provide one command that installs dependencies, checks style, analyzes, tests and runs the project. Run it from a clean environment using the committed lock file.
Beginners can add a second input form and validation. Intermediate learners can add a PSR-compatible adapter or plugin. Advanced learners can add queues, timeouts, caching, profiling or a production framework adapter. Research learners can preregister a hypothesis, compare implementations, report uncertainty and publish machine-readable evidence.
Finish with an engineering report describing one defect, the violated invariant, minimized reproducer, repair and regression. Record limitations and a question not answered. The project is complete when another person can reproduce it without private instructions.
Review checklist
Confirm that you can explain PHPUnit, predict examples, separate language and implementation behavior, validate external data, preserve ownership and cleanup, use safe database and output practices, test normal and failing behavior, and record compatibility assumptions.
Confirm that resources are bounded, secrets protected, dangerous interpretation avoided, dependencies reproducible and performance measured. Retain practice answers, project, commands, raw evidence and regression cases.
Summary
PHPUnit belongs to asynchronous execution, workers, networking and web components. PHPUnit develops evidence through testing, analysis, models, counterexamples and explicit limits of each verification technique. Dependable work begins with an explicit contract, validated boundaries, typed and cohesive interfaces, deterministic resources, meaningful exceptions and observable behavior.
Examples are a starting point. Boundary tests, failure injection, static analysis, integration work, security review and measurement provide wider evidence. Completion means being able to reproduce results and distinguish PHP language behavior from Zend Engine, extension, SAPI, framework and platform behavior.
Sources and further reading
- PHP Manual — Language Reference, security guidance and extension documentation.
- PHP supported-versions and migration documentation — release support and compatibility.
- PHP RFCs and php-src — language evolution and implementation source.
- PHP-FIG — accepted PHP Standard Recommendations and workflow status.
- Composer documentation — dependency, autoloading and package metadata contracts.
- OWASP guidance and applicable protocol specifications — web security controls.
Continue learning