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
69: Test-Driven Development in PHP
By now, you've probably heard that Test-Driven Development (TDD) is the "correct" way to write professional software. In reality, the first time you try it, it feels like you're trying to build a house by staring at a blueprint and pretending the walls exist. It feels backwards. But once it clicks, you'll realize it's not about testing—it's about design.
I get that TDD is "test first," but how does that actually work if I haven't written any logic yet?
It feels like a paradox, right? You're writing a test for a function that doesn't even exist. That's exactly the point. This is the "Red" phase of the Red-Green-Refactor cycle. You define the behavior you want before you worry about the implementation.
Let's say we're building a DiscountCalculator. I want it to take a total price and a discount percentage and return the new price. Instead of writing the class, I write the test first:
public function testItCalculatesTenPercentDiscount()
{
$calculator = new DiscountCalculator();
$result = $calculator->apply(100, 10);
$this->assertEquals(90, $result);
}
When you run this, PHPUnit will scream at you because DiscountCalculator doesn't exist. That's your "Red" light. Now, you write the absolute minimum amount of code to make that test pass. Not a line more. I don't care if the code is ugly; I just want it green.
class DiscountCalculator
{
public function apply($total, $percent)
{
return $total - ($total * ($percent / 100));
}
}
Now it's "Green." You've proven the core requirement works. Now you move to "Refactor," where you clean up the code, improve naming, or optimize, knowing that if you break something, the test will immediately catch you.
Do I really need to write a test for every single line of code? That seems like it would double my workload.
This is the most common complaint I hear from juniors. Here is the secret: TDD isn't about 100% code coverage; it's about 100% behavior coverage. You aren't testing lines of code; you're testing requirements.
If you have a simple getter or setter, don't waste your time testing it. But if you have a complex conditional—like "if the user is a VIP and it's Tuesday, give them 20% off, unless the item is already on sale"—that's where TDD shines. I've spent entire days debugging a single "edge case" that a 5-line test would have caught in milliseconds. The "extra" time you spend writing tests upfront is actually just borrowing time from the "frantically debugging at 2 AM" phase of the project.
What happens when my class needs a database or an API? I can't boot up a whole server just for a unit test.
If your test needs a database to run, you're not writing a unit test; you're writing an integration test. In TDD, we want our unit tests to be lightning-fast. To do that, we use Dependency Injection and Mocks.
Imagine our DiscountCalculator needs to check a database to see if a coupon code is valid. Don't pass the database connection into the calculator. Instead, pass an interface.
class DiscountCalculator
{
private $couponRepo;
public function __construct(CouponRepositoryInterface $repo)
{
$this->couponRepo = $repo;
}
public function apply($total, $code)
{
if (!$this->couponRepo->isValid($code)) {
return $total;
}
// apply discount logic...
}
}
In your test, you don't use the real MySQLCouponRepository. You create a "Mock"—a fake object that mimics the interface and returns exactly what you tell it to. This isolates your logic from the infrastructure. If the test fails, you know the bug is in your calculator, not because the database server is down.
When do I actually do the "refactor" part? I usually just stop once the test turns green.
I'll be honest: when I first started, I did the same thing. I'd get the test to pass and move on. But "Green" code is often "quick and dirty" code. TDD gives you a safety net that allows you to be aggressive with your refactoring.
After the test passes, look at your code. Are the variable names vague? Is there a magic number that should be a constant? Is the method too long? Because you have a passing test, you can rewrite the entire internals of that method. If you finish and the test is still green, you've successfully improved the design without changing the behavior. That's the magic of TDD—it separates the "making it work" phase from the "making it right" phase.
📋 Practical Task
Build a Tiered Membership Pricing Engine
Your goal is to build a PricingEngine class using a strict TDD workflow. You must write the tests first, run them to see them fail, and then implement the minimum code necessary to make them pass.
Requirements:
- The engine should have a method
calculatePrice(float $basePrice, string $membershipLevel). - Bronze members get a 5% discount.
- Silver members get a 10% discount.
- Gold members get a 20% discount.
- Any other membership level (or an empty string) should result in the original base price (no discount).
Your Workflow:
- Write a test for the Bronze discount $\rightarrow$ Run it (Fail) $\rightarrow$ Implement logic (Pass).
- Write a test for the Silver discount $\rightarrow$ Run it (Fail) $\rightarrow$ Implement logic (Pass).
- Write a test for the Gold discount $\rightarrow$ Run it (Fail) $\rightarrow$ Implement logic (Pass).
- Write a test for an Invalid/Guest level $\rightarrow$ Run it (Fail) $\rightarrow$ Implement logic (Pass).
- Refactor your
calculatePricemethod to remove any repetitive logic or use a lookup table/array instead of multipleifstatements.
There are no comments for now.