Why tests without mock objects are like cooking without salt
\n\nImagine you are writing an application that sends email notifications, works with a database, and accesses a third-party API. How would you test this without mock objects and fixtures in PHP? You would have to send real emails every time, create test records in the database, and wait for a response from an external service. This is slow, unreliable, and simply foolish.
\n\nThis is where test doubles come to the rescue — substitutes for real objects. In the PHP ecosystem, a combination is used: mock objects for verifying interactions and fixtures for preparing data. Let's figure out how this works in practice.
\n\nMock vs Stub vs Fake: don't confuse apples with oranges
\n\nBefore diving into the code, let's clarify the terminology. In the world of PHP unit testing, there are three main types of substitutes:
\n\n- \n
- Stub — returns predefined data. You verify that your code processes this data correctly. \n
- Mock — not only returns data but also checks how your system calls methods: how many times, with what arguments, in what order. \n
- Fake — a simplified working implementation (e.g., an in-memory database). \n
In PHPUnit, which is the de facto standard for testing PHP, these concepts are often mixed up. But we will be precise: mock objects in PHP are those where you set expectations.
\n\nWriting your first mock object with PHPUnit
\n\nLet's say we have a Mailer class that sends emails. We want to verify that the registration service calls the send() method exactly once with the correct data.
<?php\nuse PHPUnit\\Framework\\TestCase;\n\nclass UserRegistrationTest extends TestCase\n{\n public function testSendWelcomeEmailOnRegistration(): void\n {\n // Create a mock for Mailer\n $mailer = $this->createMock(Mailer::class);\n \n // Set up the expectation: the send method will be called 1 time\n $mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->equalTo('user@example.com'),\n $this->stringContains('Welcome')\n );\n \n $service = new UserRegistrationService($mailer);\n $service->register('user@example.com', 'password123');\n }\n}\n\n\nNote: we are not checking that the email was actually sent. We are checking that our code attempted to send it — this is the essence of mock objects in PHP tests. If the send() method is not called (or is called twice), the test will fail.
The expects method and its capabilities
\n\nPHPUnit provides a rich API for setting expectations:
\n\n- \n
$this->once()— exactly one call \n $this->exactly(3)— exactly three calls \n $this->atLeastOnce()— at least one call \n $this->never()— the method should not be called \n
For arguments, use matchers (constraints):
\n\n$mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->callback(function($email) {\n return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;\n }),\n $this->anything() // we don't care about the second argument\n );\n\n\nFixtures in PHP: preparing data before you need it
\n\nFixtures are a predefined state of data in which the system is placed before executing a test. In PHPUnit, the setUp() and