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
231: Introducing Tests to Untested Legacy Code
How do I even start when the code is a total mess and I'm terrified of breaking something?
I've been there. You're staring at a 500-line method in a class called OrderManager that handles everything from database inserts to sending confirmation emails. You want to fix a bug, but you have no idea what else will break if you change a single line. This is where you use "Characterization Tests."
Forget about "correctness" for a second. Your goal isn't to test if the code does what it should do, but to document what it actually does. I call this the "Golden Master" approach. You feed the method a set of inputs, capture the output (or the state of the database), and save that as your baseline. If you change the code and the output changes, you've found a regression.
// Legacy code you're afraid to touch public function processOrder($orderId) { $order = $this->db->find($orderId); $total = $order->amount * 1.15; // Hardcoded tax? Yikes. $this->db->update($orderId, ['total' => $total, 'status' => 'processed']); return "Order $orderId processed with total $total"; } // Your first characterization test public function testProcessOrderCurrentBehavior() { $service = new OrderManager(); // I don't care if this is 'wrong', I just want to know if it changes $result = $service->processOrder(123); $this->assertEquals("Order 123 processed with total 115", $result); }It feels wrong to assert something that might be a bug, but trust me: you need a safety net before you start swinging the hammer.
The code is tightly coupled to the database—how do I test it without hitting the live DB?
This is the classic legacy PHP struggle. You can't run a unit test if the constructor does
$this->db = new MySQLi(...);because your tests will suddenly depend on a running database, which makes them slow and brittle.You need to create a "seam." The easiest way is to stop the class from creating its own dependencies and start asking for them instead. This is Dependency Injection, but we're doing it as a surgical strike. I usually start by adding an optional parameter to the constructor to avoid breaking every other file in the project that instantiates this class.
// BEFORE: Hardcoded dependency class OrderManager { public function __construct() { $this->db = new DatabaseConnection(); } } // AFTER: Introducing a seam class OrderManager { private $db; public function __construct(DatabaseConnection $db = null) { // If no DB is passed (production), use the old way. // If a DB is passed (test), use the mock. $this->db = $db ?: new DatabaseConnection(); } }Now, in your test, you can pass in a mock object. You've effectively decoupled the business logic from the infrastructure without having to rewrite the entire application architecture in one afternoon.
Shouldn't I just rewrite the messy parts first so they're actually testable?
Absolutely not. This is the most dangerous trap in software engineering. I've seen countless developers spend two weeks "cleaning up" a legacy module, only to realize they've accidentally deleted a critical edge-case fix that was implemented five years ago by someone who is no longer with the company.
The rule is: Never rewrite without tests. If the code is too ugly to test, use the "Extract Method" refactor. Take a chunk of the logic that doesn't touch the database—like a tax calculation—and move it into its own small, public method. You can test that small method in isolation immediately. Once that's covered, move to the next chunk.
You aren't aiming for perfection right now; you're aiming for coverage. Once the legacy code is wrapped in a blanket of tests, you can rewrite it with the confidence that you aren't breaking the business.
📋 Practical Task
Refactoring the Legacy InvoiceCalculator
You have been handed a legacy class called InvoiceCalculator. It currently calculates a final price but is impossible to unit test because it instantiates a CurrencyConverter class directly inside its method, which makes an external API call to get live exchange rates.
class InvoiceCalculator { public function calculateTotal($amount, $currency) { $converter = new CurrencyConverter(); // The problematic dependency $rate = $converter->getRate($currency); return $amount * $rate; } }Your Task:
- Modify the
InvoiceCalculatorclass to introduce a "seam" using the constructor pattern discussed in the lesson. - Ensure that the class still works for existing production code (it should still create a
CurrencyConverterif none is provided). - Write a PHPUnit test case that passes a mocked
CurrencyConverterinto theInvoiceCalculatorto verify that thecalculateTotalmethod correctly multiplies the amount by the rate returned by the mock.
There are no comments for now.