Mock objects and fixtures in PHP tests: how to keep your sanity while writing unit tests

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why tests without mock objects are like cooking without salt

\n\n

Imagine 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\n

This 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\n

Mock vs Stub vs Fake: don't confuse apples with oranges

\n\n

Before 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
\n\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\n

Writing your first mock object with PHPUnit

\n\n

Let'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.

\n\n
<?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\n

Note: 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.

\n\n

The expects method and its capabilities

\n\n

PHPUnit 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
\n\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\n

Fixtures in PHP: preparing data before you need it

\n\n

Fixtures are a predefined state of data in which the system is placed before executing a test. In PHPUnit, the setUp() and

Blogs

Book Recommendations