Checkout::place($order, $email, $country) gets a rate from TaxRates, emails a confirmation through Mailer and returns the gross total. Both are interfaces injected through the constructor (Subsection 4.9.15), so a test replaces them with test doubles.
<?php
use PHPUnit\Framework\TestCase;
use Shop\{Checkout, Mailer, Money, Order, Product, TaxRates};
final class CheckoutTest extends TestCase {
public function testPlacingAnOrderAddsTaxAndSendsOneEmail(): void {
$rates = $this->createStub(TaxRates::class); // canned answers only
$rates->method('rateFor')->willReturn(0.2);
$mailer = $this->createMock(Mailer::class); // verifies the call
$mailer->expects($this->once())->method('send')
->with('ana@example.com', 'Order confirmed', $this->stringContains('USD 95.76'));
$order = new Order();
$order->add(new Product('BK-PHP-01', 'Modern PHP in Practice', new Money(3990), 25), 2);
$gross = new Checkout($rates, $mailer)->place($order, 'ana@example.com', 'GB');
$this->assertSame(9576, $gross->cents);
}
}A stub only feeds values in; a mock also fails the test if its expectations (once(), never(), exactly(2), with()) are not met. PHPUnit 12 79,198 removed expectations on stubs; since 12.5 a mock with no expects() earns a "No expectations were configured" notice suggesting a stub; and 13 hard-deprecates the any() matcher and adds withParameterSetsInOrder() and seal(). Stub what you query, mock what you command; PHPUnit cannot double a final class, so depend on interfaces.