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
70: Mocking with PHPUnit
Alright, let's look at a common headache I ran into early in my career. I was working on an order processing system, and every time I ran my test suite, it tried to send actual emails to customers. Not only was it slow, but I accidentally spammed a few test accounts because I forgot to change the environment variables. We can't have that.
The goal here is to test the OrderProcessor logic—making sure the order is marked as "paid" and the total is correct—without actually triggering the EmailService. In a perfect world, the OrderProcessor shouldn't even know how the email is sent; it just knows it needs to tell some service to do it.
The problem with real dependencies
Here is the setup I'm working with. I've got a simple EmailService that handles the heavy lifting of connecting to an SMTP server, and an OrderProcessor that uses it.
class EmailService {
public function sendConfirmation(string $email, string $orderId): bool {
// Imagine a lot of complex network code here
echo "Sending real email to $email...";
return true;
}
}
class OrderProcessor {
private $mailer;
public function __construct(EmailService $mailer) {
$this->mailer = $mailer;
}
public function completeOrder($order): void {
$order->status = 'completed';
$this->mailer->sendConfirmation($order->customerEmail, $order->id);
}
}
Now, if I write a standard PHPUnit test and pass in a real EmailService, my test will actually try to "send" that email. If the SMTP server is down, my test fails. If the internet is slow, my tests crawl. That's a "fragile test," and we want to avoid that at all costs.
Swapping the real deal for a double
I don't want a real EmailService, but OrderProcessor requires one in its constructor. This is where mocking comes in. Instead of writing a whole new "FakeEmailService" class manually, I can tell PHPUnit to generate one for me on the fly using createMock().
Let's see what happens when I just create a basic mock without any instructions:
public function testCompleteOrderSendsEmail() {
$mockMailer = $this->createMock(EmailService::class);
$processor = new OrderProcessor($mockMailer);
$order = (object)['id' => '123', 'customerEmail' => 'test@example.com', 'status' => 'pending'];
$processor->completeOrder($order);
$this->assertEquals('completed', $order->status);
}
This passes! But there's a problem. The test passes even if I delete the line $this->mailer->sendConfirmation(...) from the OrderProcessor class. Why? Because the mock object is just a hollow shell. It accepts any method call and returns null by default. It's not actually verifying that the email was sent; it's just staying out of the way.
Demanding a specific interaction
To make this test actually useful, I need to move from "just ignore the mailer" to "verify the mailer was used correctly." I want to ensure that sendConfirmation is called exactly once, and that it's called with the right email address.
I'll adjust the test to set an expectation:
public function testCompleteOrderSendsEmail() {
$mockMailer = $this->createMock(EmailService::class);
// Here is where the magic happens
$mockMailer->expects($this->once())
->method('sendConfirmation')
->with('test@example.com', '123')
->willReturn(true);
$processor = new OrderProcessor($mockMailer);
$order = (object)['id' => '123', 'customerEmail' => 'test@example.com', 'status' => 'pending'];
$processor->completeOrder($order);
}
Now, the test is much stricter. If I comment out the mailer call in the OrderProcessor, PHPUnit will throw an exception: "Expectation failed fulfilling specified invocation count." It's basically saying, "You promised me this method would be called once, but it never happened."
Handling different return values
Sometimes we need to test how our code handles failure. What if the email service returns false? If my OrderProcessor had logic to log a failure or throw an exception when the email fails, I could test that simply by changing the mock's behavior.
I just change ->willReturn(true) to ->willReturn(false). I don't have to manually break my SMTP server to see if my error handling works; I just tell the mock to lie to the OrderProcessor. This is the real power of mocking: you control the universe your class lives in during the test.
📋 Practical Task
Exercise: Verifying the Payment Gateway Integration
You have a PaymentProcessor class that depends on a PaymentGateway. You need to write a test that ensures the PaymentGateway's charge() method is called exactly once with the correct amount and currency, and that the PaymentProcessor returns true when the gateway confirms the charge.
The classes are already provided:
class PaymentGateway {
public function charge(float $amount, string $currency): bool {
// Connects to an external API (Stripe, PayPal, etc.)
return true;
}
}
class PaymentProcessor {
private $gateway;
public function __construct(PaymentGateway $gateway) {
$this->gateway = $gateway;
}
public function processPayment(float $amount): bool {
// We assume USD for this simple processor
return $this->gateway->charge($amount, 'USD');
}
}
Your Task: Create a PHPUnit test method that mocks the PaymentGateway, verifies the charge method is called with (99.99, 'USD'), and asserts that processPayment(99.99) returns true.
There are no comments for now.