Testing With PHPUnit
Every challenge across this entire series has been verified by reading expected output and reasoning about the code. PHPUnit automates that verification — writing assertions once, then re-running them in seconds, every time the code changes, to catch regressions immediately rather than discovering them later.
Installing PHPUnit
A First Unit Test
A test class extends TestCase (Intermediate Chapter 2's inheritance, applied here). Each method starting with test is run automatically, and $this->assertEquals(expected, actual) checks the result matches — exactly the same exercise as every challenge's "expected output" comment throughout this course, just executed by PHPUnit instead of read by eye.
Common Assertions
| Assertion | Checks |
|---|---|
| assertEquals($expected, $actual) | Values are equal (loose comparison) |
| assertSame($expected, $actual) | Values AND types match (strict, like ===) |
| assertTrue() / assertFalse() | A value is exactly true / false |
| assertCount($count, $array) | An array has exactly $count elements |
| assertInstanceOf($class, $obj) | An object is an instance of a given class |
| expectException($class) | The tested code throws a specific exception |
Testing That an Exception Is Thrown
This directly tests the kind of behaviour built in Intermediate Chapter 3 — confirming the right exception type is genuinely thrown under the right conditions, automatically, rather than manually re-checking by eye every time the code changes.
The Problem Mocking Solves
The Intermediate capstone's Post class depends on a real PDO connection. Testing it directly would mean hitting an actual database — slow, and dependent on test data that could change. A mock is a fake stand-in object that behaves exactly as instructed, without needing the real dependency at all.
Mocking a Dependency
createMock(UserRepository::class) generates a fake object implementing that interface. ->method('find')->willReturn([...]) programs exactly what calling find() on it should produce — no real database, no real network call, completely predictable and fast. greetUser() from Chapter 2 never needed to be written knowing it would be tested this way — it already depended only on the UserRepository interface, which is precisely what makes mocking it possible at all.
PDO object directly is much harder to mock cleanly. Code written against an interface (UserRepository) can have any implementation substituted in — a real one in production, a fake/mock one in tests — without the calling code needing to change or even know the difference.
Running the Whole Test Suite
Coding Challenges
Write a class StringHelper with a method reverse(string $s): string (using strrev()) and a method isPalindrome(string $s): bool. Write a PHPUnit test class with at least three test methods covering: a normal reverse, a true palindrome case, and a false (non-palindrome) case.
Using the Chapter 3 InsufficientFundsException-style BankAccount class from Intermediate Chapter 3, write a PHPUnit test that uses expectException() to confirm withdrawing more than the balance throws InsufficientFundsException, and a second test confirming a valid withdrawal correctly reduces the balance.
📄 View solutionUsing the Strategy pattern's ShoppingCart from Chapter 2, write a test that uses createMock() to create a fake DiscountStrategy whose apply() method is programmed (via willReturn) to always return 50, then asserts that ShoppingCart's checkout() correctly returns 50 regardless of the price passed in. Explain in a comment why this test never touches PercentageDiscount or FixedAmountDiscount at all.
📄 View solutionChapter 4 Quick Reference
- composer require --dev phpunit/phpunit — install as a dev dependency
- extends TestCase, methods starting with "test" are run automatically
- assertEquals / assertSame / assertTrue / assertCount / assertInstanceOf — common assertions
- expectException(ClassName::class) — confirms specific code throws the expected exception
- createMock(InterfaceName::class) — a fake stand-in object, no real dependency needed
- ->method('name')->willReturn(value) — programs a mock's behaviour
- Code written against interfaces is far easier to mock than code tied directly to a concrete class
- Next chapter: security in depth — CSRF, XSS prevention, password hashing