PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
43: Unit Testing with PHPUnit
I've spent the last hour writing a DiscountCalculator class for a client project. It's not a huge piece of logic, but it has a few rules: a percentage discount, a flat-rate discount, and a minimum spend requirement. Right now, my "testing process" looks like this:
<?php
require 'DiscountCalculator.php';
$calc = new DiscountCalculator();
var_dump($calc->apply('PERCENT', 100, 20)); // I expect 80
var_dump($calc->apply('FLAT', 100, 20)); // I expect 80
var_dump($calc->apply('PERCENT', 50, 20)); // I expect 50 (because min spend is 60)
?>
I'm staring at the terminal, manually checking if the output is correct. It works for now, but I'm terrified to touch this code in six months. If I change one line to optimize the logic, I have to manually run these var_dump statements again. There has to be a better way to prove the code works without me acting as a human calculator.
Moving beyond the var_dump
I'm going to pull in PHPUnit. If you haven't used it yet, it's the industry standard. First, I'll get it into the project via Composer. I use the --dev flag because I don't need the testing framework running on the actual production server.
composer require --dev phpunit/phpunit
Now, instead of a random script, I'll create a dedicated test file. PHPUnit looks for classes that extend TestCase and methods that start with the word test. Let's try to automate that first check: the percentage discount.
<?php
use PHPUnit\Framework\TestCase;
class DiscountCalculatorTest extends TestCase {
public function testApplyPercentageDiscount() {
$calc = new DiscountCalculator();
$result = $calc->apply('PERCENT', 100, 20);
$this->assertEquals(80, $result);
}
}
I'll run it from the vendor folder: ./vendor/bin/phpunit DiscountCalculatorTest.php. I get a big green OK. It feels great, but it's a bit boring. The real value comes when things break.
Finding the edge cases
I started thinking: what happens if a user passes a negative discount? Or a negative total? My current code probably just does the math and returns a weird number. Let's see. I'll add a test for a negative discount.
public function testNegativeDiscountThrowsException() {
$calc = new DiscountCalculator();
// I expect this to fail or throw an error, but let's see what happens
$result = $calc->apply('PERCENT', 100, -10);
$this->assertEquals(110, $result);
}
The test passes. Wait—it passes? That's actually a failure in my business logic. The calculator just added 10% to the price because I passed a negative number. My code is "correct" according to the test, but "wrong" according to the real world. This is exactly why we unit test; it forces me to define what "correct" actually means.
I should be throwing an InvalidArgumentException here. Let me adjust the test first to reflect how the code should behave, and then I'll fix the code. This is a common workflow: write the failing test, then write the code to make it pass.
public function testNegativeDiscountThrowsException() {
$this->expectException(InvalidArgumentException::class);
$calc = new DiscountCalculator();
$calc->apply('PERCENT', 100, -10);
}
Now, when I run PHPUnit, it fails. It tells me Failed asserting that exception of type "InvalidArgumentException" is thrown. Now I can go into DiscountCalculator.php and actually add the validation logic to throw that exception. Once I save the file and run the test again... OK. The green light is back.
Testing the "Invisible" Rules
The last bit of logic is the minimum spend. The rule is: "If the total is under $60, no discount is applied." I've been testing the "happy path" (where it works), but I need to test the boundaries. What happens at exactly $59.99? What happens at exactly $60.00?
I'll use a dataProvider. I don't want to write five different methods that all do the same thing. I can just feed a list of scenarios into one test method.
public function discountDataProvider() {
return [
'below_min_spend' => [50, 20, 50], // Total 50, Discount 20%, Result should stay 50
'at_min_spend' => [60, 20, 48], // Total 60, Discount 20%, Result should be 48
'above_min_spend' => [100, 20, 80], // Total 100, Discount 20%, Result 80
];
}
/**
* @dataProvider discountDataProvider
*/
public function testMinSpendLogic($total, $discount, $expected) {
$calc = new DiscountCalculator();
$this->assertEquals($expected, $calc->apply('PERCENT', $total, $discount));
}
This is where PHPUnit becomes powerful. Instead of one test, I now have three distinct scenarios running through the same logic. If I accidentally change the minimum spend to $70 in the code, the at_min_spend test will fail instantly, and I'll know exactly where the regression happened.
📋 Practical Task
Refactoring the TaxCalculator with PHPUnit
You have been handed a TaxCalculator class that calculates sales tax based on a region code. However, the current implementation is buggy and has no tests. Your goal is to bring it under control using PHPUnit.
The Requirements:
- Region 'US' should apply 7% tax.
- Region 'EU' should apply 20% tax.
- Any unknown region should throw an
UnexpectedValueException. - The calculator should not allow negative price inputs.
Your Task:
- Create a
TaxCalculatorTestclass. - Write a test method using a
dataProviderto verify the tax rates for 'US' and 'EU'. - Write a test to ensure an
UnexpectedValueExceptionis thrown when an invalid region (e.g., 'JP') is provided. - Write a test to ensure an
InvalidArgumentExceptionis thrown if a negative price is passed to the calculator. - Run the tests, observe the failures, and modify the
TaxCalculatorlogic until all tests pass.
There are no comments for now.