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.
Quick recap:
PracticeCovered in
One focused behavior per test methodYour First Test
Fresh state via setUp()Test Lifecycle
Mock external dependencies in unit testsMocking Objects
Data providers instead of copy-pasting testsData Providers
Coverage as a signal, not a targetCode Coverage
Run the full suite before every commit/deployThis page
Keep the suite fastThis page
You've completed the PHPUnit track! Congratulations on working through everything from your first automated test to assertions, the test lifecycle, exceptions, data providers, mocking, coverage, and organizing a growing suite. This same testing discipline carries straight into the Slim track already on this site, and directly into Laravel and Symfony as those tracks arrive — the framework changes, but writing small, focused, well-isolated tests doesn't. Click Next below to head back to the PHP hub and pick another track to continue learning.