Stubs and Mocks

Test Doubles: Stubs and Mocks

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.

tests/Unit/CheckoutTest.php, a stub for input and a mock for the outgoing callPHP
<?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.