Best Practices
This page is a capstone checklist for the whole PHPUnit track — the habits worth carrying into every real project. Each item links back to the page that covers it in full, so use this as a quick-reference summary rather than a first read.
1) Test One Thing Per Test Method
Keep each test method small and focused on a single behavior, with a descriptive name that says
exactly what it verifies. testAddsTwoNumbers() tells you what broke the moment it fails;
testCalculator() does not. See Your First Test.
<?php
// Good: name says exactly what's being verified
public function testAddingNegativeNumbersReturnsNegativeSum()
{
$this->assertSame(-5, add(-2, -3));
}
- One logical behavior per test method — split it up if a name needs "and" to describe it.
- Name tests after the behavior they check, not after the method under test.
2) Use setUp() for Fresh State
Never let one test's leftover state leak into the next. setUp() runs before every single
test method, so rebuilding shared objects there guarantees each test starts from a clean, predictable
slate. See Test Lifecycle.
<?php
protected function setUp(): void
{
// Fresh instance before every test — never shared or reused
$this->calculator = new Calculator();
}
- Rebuild the object under test in
setUp()rather than a shared property set once. - Never rely on one test running before another — each must pass in isolation, in any order.
3) Mock External Dependencies in Unit Tests
Databases, third-party APIs, and the filesystem are slow and unreliable to depend on inside a unit test. Mock them so the test exercises only your own class's logic, runs in milliseconds, and never fails because an external service happened to be down. See Mocking Objects.
<?php
$mockGateway = $this->createMock(PaymentGatewayInterface::class);
$mockGateway->expects($this->once())->method('charge');
- Mock the database, external APIs, and the filesystem in unit tests.
- Save real dependencies for a separate integration test suite.
4) Use Data Providers Instead of Copy-Pasting
The moment you find yourself duplicating a test method just to change the input and expected value, that's the signal to reach for a data provider instead. One test method, many rows of cases, no duplication. See Data Providers.
<?php
#[DataProvider('numberProvider')]
public function testIsEven(int $n, bool $expected)
{
$this->assertSame($expected, isEven($n));
}
- Reach for a data provider the second time you'd otherwise copy-paste a test method.
- Give each data set a descriptive string key so failures are easy to read.
5) Treat Coverage as a Signal, Not a Target
Code coverage tells you which lines never ran during testing — genuinely useful information. It does not tell you whether the lines that did run were actually verified correctly. Chasing a coverage percentage as the goal itself encourages tests that touch code without meaningfully checking it. See Code Coverage.
./vendor/bin/phpunit --coverage-html coverage/
- Use a coverage report to find untested code, not as a scoreboard to maximize.
- 100% coverage with weak assertions is worse than 80% coverage with strong ones.
6) Run the Full Suite Before Every Commit or Deploy
A test suite only protects you if it's actually run. Get in the habit of running the whole suite before every commit, and automate that same run in your CI pipeline so it happens even if someone forgets locally.
# Run the whole suite before committing
./vendor/bin/phpunit
# In CI: fail the build if any test fails
./vendor/bin/phpunit --stop-on-failure
- Run the full suite locally before every commit, not just the file you touched.
- Wire the same command into CI so a forgotten local run still gets caught.
7) Keep the Suite Fast
A test suite that takes ten minutes to run is a test suite developers learn to skip under deadline pressure — and a skipped suite protects nobody. Fast, well-mocked unit tests are what keep the whole team actually running tests instead of quietly disabling them "just this once."
- Favor many small, fast unit tests over a few slow ones that hit real infrastructure.
- If the suite is getting slow, split it into a quick suite for local development and a fuller one for CI, rather than letting everyone stop running it at all.
| Practice | Covered in |
|---|---|
| One focused behavior per test method | Your First Test |
Fresh state via setUp() | Test Lifecycle |
| Mock external dependencies in unit tests | Mocking Objects |
| Data providers instead of copy-pasting tests | Data Providers |
| Coverage as a signal, not a target | Code Coverage |
| Run the full suite before every commit/deploy | This page |
| Keep the suite fast | This page |