Glossary
PHPUnit
A testing framework for PHP that provides fast feedback on the behaviour of small parts of an application and selected integrations.
Short definition
A tool for automated verification of PHP applications.
PHPUnit provides a test class, assertions for comparing expected and actual results, support for expected exceptions, and a runner that discovers and runs tests. Its results can be used locally, in an IDE, and as a required check in CI.
The name does not mean that every test run through PHPUnit is a unit test. The same framework can run an isolated domain service test, an integration test involving a database or HTTP, a feature test, or a contract test. The type of test is determined by its scope and dependencies, not by the command used to run it.
Use cases
When to verify a PHP application automatically
A test should verify a specific observable rule or interaction that a future change could break.
- domain rules and calculations for prices, discounts, reservations, and order states
- edge cases in imports, validation, and expected exceptions
- repositories, caches, or HTTP integrations in a test environment
- regressions of bugs fixed in an e-commerce site or internal system
- fast feedback on a pull request in CI
Practical example
Replenishing only the stock that is actually missing
Suppose there is a ReorderQuantity domain class with a forAvailableStock(int $available, int $target): int method. Calculating stock replenishment is a small domain rule. The test verifies the method’s result, not the private steps the calculator performs internally. It is fast, has no network or database dependencies, and is a unit test even though PHPUnit runs it.
If the test verified persistence in a database or sending a request to a marketplace instead of the calculation itself, it would be an integration or communication scenario. The chosen assertions should match that contract.
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ReorderQuantityTest extends TestCase
{
#[Test]
public function it_orders_only_the_missing_stock(): void
{
$calculator = new ReorderQuantity();
self::assertSame(12, $calculator->forAvailableStock(8, 20));
}
}
How it works
From a test scenario to a run result
A test describes a verifiable example of behaviour; the runner executes it reproducibly in a prepared environment.
- Test discovery The runner loads test suites from phpunit.xml or a selected file, group, or command-line filter.
- Setup A fixture creates the required data and dependencies; setUp() and tearDown() should handle only essential shared state.
- Execution The test calls the object under test or an integration boundary with specific inputs.
- Verification Assertions compare the result, side effect, or expected exception with the contract.
- Report and CI The runner returns a result, and a non-zero exit code can stop a required pull request check.
Main components
Test cases, assertions, and controlled scenarios
PHPUnit provides the basic building blocks; how they are used should follow from the contract being verified.
Test cases and assertions
A test class extends TestCase. A method identified by a test name or the #[Test] attribute verifies a result, for example through assertSame(), a value’s structure, or an exception.
Data providers
One test can run against multiple input sets. This is useful for validation boundaries or pricing rules, but not for hiding several unrelated scenarios in one method.
Suites, groups, and configuration
phpunit.xml usually defines the bootstrap, test suites, and reports. Groups and filters shorten local feedback, while CI generally runs the designated complete suite.
Fixtures
setUp() and tearDown() help with shared setup and cleanup, but an overly extensive shared fixture obscures what the test actually needs.
Benefits and limitations
Good tests protect behaviour, not implementation details
Benefits
- fast, repeatable verification of rules before merging
- regressions caught close to the change
- expected behaviour documented clearly for future development
- the option to combine fast unit tests with important integration scenarios
Common mistakes
- mocking every collaborator and verifying internal calls instead of results
- shared state between tests or dependence on execution order
- slow and unstable tests waiting on a network or time
- chasing a coverage percentage without verifying important decisions
Scope of use
A test should be as isolated as the contract being verified allows.
A mock makes sense when communication with a port is important—for example, verifying that the application sends an order for fulfilment after a successful payment. It should not be used merely to replace every object in a test. A test too closely tied to call counts and helper methods will break even during safe refactoring.
Code coverage shows which parts of the code tests executed; it does not prove that assertions are correct, scenarios are meaningful, or all business risks are covered. A slow test usually indicates overly broad scope or uncontrolled infrastructure, while a flaky test points to a race, shared state, or nondeterministic input that should be removed rather than merely retried.
What to consider
What keeps a test suite useful
A good suite must be runnable frequently and provide a clear reason for each failure.
- a test name describing an observable rule or scenario
- explicit inputs and minimal hidden fixture state
- separate fast unit tests and targeted integration suites
- deterministic time, randomness, and test data
- running the relevant suite locally and as a required CI check
- coverage as a directional signal, not a developer performance metric
Common questions
What PHPUnit can and cannot do
Is every test run through PHPUnit a unit test?
No. PHPUnit is a framework and runner. Unit, integration, feature, contract, and other tests differ in the scope of tested behaviour and the dependencies they use.
When should I use a stub and when a mock?
Use a stub for controlled input from a dependency. Use a mock when communication with that dependency is itself part of the contract, such as sending a command with specific data.
Is high code coverage enough?
No. Coverage shows executed code, but it does not say whether assertions verify the right rule or whether the suite includes important error states and integrations.
Why are some tests slow or unreliable?
They often depend on a network, time, randomness, a shared database, or execution order. Such inputs should be controlled, isolated, or covered by a targeted integration environment.
Should PHPUnit run in CI?
Yes, usually over a designated test suite for every pull request. CI should fail with a clear report, not merely rerun tests without addressing their instability.
How I use testing in practice
I combine tests with static and architectural checks.
On my experience page, I describe how I choose unit and integration tests for different kinds of changes. The public scheduling project contains separate test suites.