chubbyphp / chubbyphp-mock
A strict mocking solution. It works with any testing framework; the examples below use PHPUnit.
Requires
- php: ^8.3
- nikic/php-parser: ^5.8
Requires (Dev)
- chubbyphp/chubbyphp-dev-helper: dev-master
- dg/bypass-finals: ^1.11.0
- infection/infection: ^0.35.4
- php-coveralls/php-coveralls: ^2.9.1
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.2.13
- phpunit/phpunit: ^12.5.34
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0.2
- symfony/symfony: ^7.4.18|^8.1.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-09 19:58:12 UTC
README
Description
A strict mocking solution. It works with any testing framework; the examples below use PHPUnit.
A mock is defined as an ordered list of expected method calls. Every call made on the mock must match the next expectation in that list by method name and parameters, otherwise an exception is thrown. Missing calls are detected as well: a mock that still has unconsumed expectations when it is destroyed throws too.
Every exception carries a JSON message with the mocked class, the position of the failing call, the actual and expected values, and the file and line where the mock was created.
Requirements
- php: ^8.3
- nikic/php-parser: ^5.8
Installation
Through Composer as chubbyphp/chubbyphp-mock.
composer require chubbyphp/chubbyphp-mock "^2.2" --dev
Usage
Create a MockObjectBuilder and pass it the class or interface to mock together with the list of expected calls.
Each expected call is an instance of one of the following mock methods:
| Mock method | Constructor | Behaviour |
|---|---|---|
WithoutReturn |
(string $name, array $parameters, bool $strict = true) |
Validates the call and returns nothing. |
WithReturn |
(string $name, array $parameters, mixed $return, bool $strict = true) |
Validates the call and returns the given value. |
WithReturnSelf |
(string $name, array $parameters, bool $strict = true) |
Validates the call and returns the mock itself (fluent APIs). |
WithException |
(string $name, array $parameters, \Throwable $exception, bool $strict = true) |
Validates the call and throws the given exception. |
WithCallback |
(string $name, callable $callback) |
Validates the method name and delegates to the callback with the actual parameters. Its return value is returned by the mock. |
Parameters are compared with === by default. Pass $strict = false to compare by value instead; arrays are then
compared entry by entry and objects property by property (or via __serialize / __sleep when available).
Use WithCallback whenever a parameter cannot be known in advance (for example a generated id or a timestamp) or when
you want to assert on it inside the callback.
<?php declare(strict_types=1); namespace MyProject\Tests\Unit\RequestHandler; use Chubbyphp\Mock\MockMethod\WithCallback; use Chubbyphp\Mock\MockMethod\WithReturn; use Chubbyphp\Mock\MockMethod\WithReturnSelf; use Chubbyphp\Mock\MockObjectBuilder; use MyProject\RequestHandler\PingRequestHandler; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; final class PingRequestHandlerTest extends TestCase { public function testHandle(): void { $builder = new MockObjectBuilder(); // no calls expected: any method call on this mock fails the test $request = $builder->create(ServerRequestInterface::class, []); $responseBody = $builder->create(StreamInterface::class, [ // the written JSON is not known in advance, so assert on it in a callback new WithCallback('write', static function (string $string): int { $data = json_decode($string, true); self::assertArrayHasKey('datetime', $data); return \strlen($string); }), ]); $response = $builder->create(ResponseInterface::class, [ // calls must happen in exactly this order new WithReturnSelf('withHeader', ['Content-Type', 'application/json']), new WithReturnSelf('withHeader', ['Cache-Control', 'no-cache, no-store, must-revalidate']), new WithReturnSelf('withHeader', ['Pragma', 'no-cache']), new WithReturnSelf('withHeader', ['Expires', '0']), new WithReturn('getBody', [], $responseBody), ]); $responseFactory = $builder->create(ResponseFactoryInterface::class, [ new WithReturn('createResponse', [200, ''], $response), ]); $requestHandler = new PingRequestHandler($responseFactory); self::assertSame($response, $requestHandler->handle($request)); } }
Other testing frameworks
chubbyphp-mock has no dependency on PHPUnit. A host framework only needs to provide a place to build the mocks,
a way to assert inside a WithCallback, and a test scope that ends when the test ends.
Unconsumed expectations are reported from the mock's destructor. Keep mocks in local variables of the test and do not store them in long-lived properties, statics or shared setup, otherwise the check is delayed and the failure is attributed to the wrong place.
The examples below use the same PingRequestHandler scenario as the PHPUnit example above.
Pest
expect() replaces the PHPUnit assertion inside the callback, everything else stays the same.
<?php declare(strict_types=1); use Chubbyphp\Mock\MockMethod\WithCallback; use Chubbyphp\Mock\MockMethod\WithReturn; use Chubbyphp\Mock\MockMethod\WithReturnSelf; use Chubbyphp\Mock\MockObjectBuilder; use MyProject\RequestHandler\PingRequestHandler; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; it('handles a ping request', function (): void { $builder = new MockObjectBuilder(); $request = $builder->create(ServerRequestInterface::class, []); $responseBody = $builder->create(StreamInterface::class, [ new WithCallback('write', static function (string $string): int { expect(json_decode($string, true))->toHaveKey('datetime'); return \strlen($string); }), ]); $response = $builder->create(ResponseInterface::class, [ new WithReturnSelf('withHeader', ['Content-Type', 'application/json']), new WithReturnSelf('withHeader', ['Cache-Control', 'no-cache, no-store, must-revalidate']), new WithReturnSelf('withHeader', ['Pragma', 'no-cache']), new WithReturnSelf('withHeader', ['Expires', '0']), new WithReturn('getBody', [], $responseBody), ]); $responseFactory = $builder->create(ResponseFactoryInterface::class, [ new WithReturn('createResponse', [200, ''], $response), ]); $requestHandler = new PingRequestHandler($responseFactory); expect($requestHandler->handle($request))->toBe($response); });
Codeception
Codeception unit tests are PHPUnit test cases with a different base class, so the PHPUnit example works unchanged.
<?php declare(strict_types=1); namespace MyProject\Tests\Unit\RequestHandler; use Chubbyphp\Mock\MockMethod\WithCallback; use Chubbyphp\Mock\MockMethod\WithReturn; use Chubbyphp\Mock\MockMethod\WithReturnSelf; use Chubbyphp\Mock\MockObjectBuilder; use Codeception\Test\Unit; use MyProject\RequestHandler\PingRequestHandler; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; final class PingRequestHandlerTest extends Unit { public function testHandle(): void { $builder = new MockObjectBuilder(); $request = $builder->create(ServerRequestInterface::class, []); $responseBody = $builder->create(StreamInterface::class, [ new WithCallback('write', function (string $string): int { $data = json_decode($string, true); $this->assertArrayHasKey('datetime', $data); return \strlen($string); }), ]); $response = $builder->create(ResponseInterface::class, [ new WithReturnSelf('withHeader', ['Content-Type', 'application/json']), new WithReturnSelf('withHeader', ['Cache-Control', 'no-cache, no-store, must-revalidate']), new WithReturnSelf('withHeader', ['Pragma', 'no-cache']), new WithReturnSelf('withHeader', ['Expires', '0']), new WithReturn('getBody', [], $responseBody), ]); $responseFactory = $builder->create(ResponseFactoryInterface::class, [ new WithReturn('createResponse', [200, ''], $response), ]); $requestHandler = new PingRequestHandler($responseFactory); $this->assertSame($response, $requestHandler->handle($request)); } }
phpspec
phpspec injects lenient, unordered Prophecy collaborators by default. Building the mocks with chubbyphp-mock instead gives the spec a strict, ordered script of expected calls.
<?php declare(strict_types=1); namespace spec\MyProject\RequestHandler; use Chubbyphp\Mock\MockMethod\WithCallback; use Chubbyphp\Mock\MockMethod\WithReturn; use Chubbyphp\Mock\MockMethod\WithReturnSelf; use Chubbyphp\Mock\MockObjectBuilder; use MyProject\RequestHandler\PingRequestHandler; use PhpSpec\ObjectBehavior; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; final class PingRequestHandlerSpec extends ObjectBehavior { public function it_handles_a_ping_request(): void { $builder = new MockObjectBuilder(); $request = $builder->create(ServerRequestInterface::class, []); $responseBody = $builder->create(StreamInterface::class, [ // phpspec has no standalone assertion API, so fail with an exception new WithCallback('write', static function (string $string): int { $data = json_decode($string, true); if (!\array_key_exists('datetime', $data)) { throw new \RuntimeException('Missing key "datetime" in written JSON'); } return \strlen($string); }), ]); $response = $builder->create(ResponseInterface::class, [ new WithReturnSelf('withHeader', ['Content-Type', 'application/json']), new WithReturnSelf('withHeader', ['Cache-Control', 'no-cache, no-store, must-revalidate']), new WithReturnSelf('withHeader', ['Pragma', 'no-cache']), new WithReturnSelf('withHeader', ['Expires', '0']), new WithReturn('getBody', [], $responseBody), ]); $responseFactory = $builder->create(ResponseFactoryInterface::class, [ new WithReturn('createResponse', [200, ''], $response), ]); $this->beConstructedWith($responseFactory); $this->handle($request)->shouldReturn($response); } }
FAQ
Howto mock final classes/methods
Use the third party package dg/bypass-finals.
This does not remove the final keyword from internal (PHP core or extension) classes.
What Cannot Be Mocked
-
Static methods: They are declared on the mock but throw when called.
-
Properties: Only method calls are intercepted.
-
__constructand__destruct: The mock defines its own constructor and destructor. -
Internal final classes or methods: Even with
dg/bypass-finals, final internal classes or methods cannot be mocked. -
Poorly built extension classes: Some older PHP extensions declare classes that cannot be fully reverse-engineered via reflection. Those classes are not mockable.
Special Handling
\Traversableand interfaces extending it: PHP does not allow a userland class to implement\Traversabledirectly; it must implement\Iteratoror\IteratorAggregateinstead. The generated mock therefore additionally implements\IteratorAggregateand, if the mocked type does not declare it, agetIterator()method. That method behaves like any other mocked method and needs a matching expectation when called.
Please report if you find other restrictions / bugs.
Upgrade
Copyright
2026 Dominik Zogg