Unit Testing
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). Tools like phpunit, pest, phpstan, and infection can flag absence of a test directory or low coverage (<40%), but they cannot reliably detect tests that hit real databases/filesystem/network, tests with no assertions, or tests tightly coupled to implementation — these require careful code review. Coverage thresholds are blunt instruments and won't catch false-confidence tests that always pass.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes injecting all dependencies so they can be mocked, applying arrange-act-assert pattern, and testing one unit of behaviour per test. Retrofitting a codebase that has no isolation, hits real databases, or has tightly coupled tests requires touching many test files and often refactoring production code to enable dependency injection — more than a single-line fix but typically contained within the testing layer and affected components.
Closest to 'persistent productivity tax' (b5). Unit testing applies across web, cli, and queue-worker contexts (broad applies_to). A codebase with poor or absent unit tests creates a persistent productivity tax: every refactor carries regression risk, onboarding is slower, and CI pipelines give false confidence. However, it doesn't define the system's architectural shape the way a database choice would — it's a sustained drag rather than a gravitational centre.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field directly identifies the canonical trap: developers believe a unit test tests a single function/method, when the defining characteristic is actually speed and isolation from external dependencies. Common mistakes compound this — tests with no assertions, tests coupled to implementation details, missing edge cases. These are well-documented gotchas that most developers eventually encounter and correct, fitting t5.
Also Known As
TL;DR
Explanation
Unit tests isolate a single class or function, replacing real dependencies (databases, HTTP clients, file systems) with test doubles (mocks, stubs, fakes). They run fast (no I/O), pinpoint failures precisely, and are the foundation of test-driven development. PHPUnit is the standard PHP unit testing framework. Well-designed code (injected dependencies, small classes) is easy to unit test — if a class is hard to test, that's a signal its design needs improvement. Aim for high coverage of business logic, edge cases, and error paths.
Common Misconception
Why It Matters
Common Mistakes
- Tests that test multiple units — failing tests give no indication of which unit is broken.
- Tests with no assertions — they pass always and provide false confidence.
- Tests tightly coupled to implementation details — refactoring breaks tests even when behaviour is correct.
- No tests for edge cases (empty input, null, zero, max values) — bugs live in boundaries.
Code Examples
// Test with no assertion — always passes:
public function testProcess(): void {
$service = new OrderService();
$service->process($order); // Called but nothing asserted
// PHPUnit reports: 1 test, 0 assertions — misleadingly green
}
// With assertions:
public function testProcess(): void {
$result = $service->process($order);
$this->assertSame('processed', $result->status);
$this->assertTrue($result->emailSent);
}
use PHPUnit\Framework\TestCase;
class MoneyTest extends TestCase {
public function test_add_same_currency(): void {
$a = new Money(100, 'GBP');
$b = new Money(50, 'GBP');
$this->assertEquals(new Money(150, 'GBP'), $a->add($b));
}
public function test_add_different_currency_throws(): void {
$this->expectException(CurrencyMismatch::class);
(new Money(100, 'GBP'))->add(new Money(100, 'USD'));
}
}