PHP Logophpspec/prophecy

phpspec/prophecy is a powerful and elegant PHP mocking framework primarily designed to be used with phpspec, a Behavior-Driven Development (BDD) framework for PHP. However, it can also be used as a standalone mocking library within other testing frameworks like PHPUnit.

Prophecy's core purpose is to help you create 'test doubles' – specifically mocks, stubs, and spies – for your dependencies during unit testing. This allows you to isolate the code under test from its collaborators, ensuring that your tests focus solely on the behavior of the unit in question, rather than the behavior of its dependencies.

Key concepts in Prophecy include:

* Prophet: This is the main entry point for Prophecy. You create an instance of `Prophet` to generate prophecies.
* Prophecy Object: When you 'prophesize' a class or interface (e.g., `$prophet->prophesize(MyDependency::class)`), Prophecy returns a `Prophecy` object. This object allows you to define expectations and behaviors for the methods of the mocked dependency.
* `reveal()`: After defining the behavior of your prophecy, you call the `reveal()` method on the `Prophecy` object. This turns the prophecy into a concrete test double (an object that implements the original interface or extends the original class) that you can then pass to your code under test.
* Method Calls and Expectations: Prophecy provides a fluent API to define how methods on your test double should behave:
* `->methodName(arguments)`: You interact with the `Prophecy` object as if it were the actual dependency. This allows you to define what happens when `methodName` is called with specific `arguments`.
* `->willReturn(value)`: Specifies the value that a method call should return.
* `->willThrow(exception)`: Specifies that a method call should throw a particular exception.
* `->shouldBeCalled()`: Asserts that a method should be called at least once.
* `->shouldBeCalledTimes(count)`: Asserts that a method should be called an exact number of times.
* `->shouldNotBeCalled()`: Asserts that a method should *not* be called.
* `->should(matcher)`: Allows for more advanced argument matching and expectation definition using `Prophecy\Argument` matchers (e.g., `Argument::any()`, `Argument::type('string')`).

Prophecy promotes a clear, fluent, and expressive syntax, making tests highly readable and maintainable. It's particularly well-suited for BDD-style testing where you describe the *behavior* of objects. By using Prophecy, developers can write robust and fast unit tests that verify the interactions between objects without relying on complex setups or actual external resources like databases or APIs.

Example Code

<?php

// First, make sure you have phpspec/prophecy installed via Composer:
// composer require --dev phpspec/prophecy

require 'vendor/autoload.php'; // Assuming Composer autoloading

use Prophecy\Prophet;
use Prophecy\Argument;

// --- 1. Define an Interface and a Class that uses it --- 

interface UserRepository
{
    public function findUserById(int $id): ?array;
    public function saveUser(array $userData): bool;
}

class UserService
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function getUserDetails(int $userId): ?array
    {
        // Business logic here, e.g., fetching user and then enriching data
        $user = $this->userRepository->findUserById($userId);

        if ($user === null) {
            return null;
        }

        // Example: add a derived field
        $user['full_name'] = ($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '');

        return $user;
    }

    public function createUser(array $userData): bool
    {
        if (!isset($userData['username']) || !isset($userData['password'])) {
            return false; // Basic validation
        }
        return $this->userRepository->saveUser($userData);
    }
}

// --- 2. Demonstrate Prophecy in a Test-like Scenario --- 

echo "--- Prophecy Example ---\n\n";

$prophet = new Prophet();

// 1. Prophesize the UserRepository interface
$userRepositoryProphecy = $prophet->prophesize(UserRepository::class);

// 2. Define behavior for findUserById:
//    - When called with ID 1, it should return a specific user array.
//    - It should be called exactly once.
$expectedUser = [
    'id' => 1,
    'first_name' => 'John',
    'last_name' => 'Doe',
    'email' => 'john.doe@example.com'
];
$userRepositoryProphecy->findUserById(1)
    ->willReturn($expectedUser)
    ->shouldBeCalledTimes(1);

// 3. Define behavior for saveUser:
//    - When called with any array, it should return true.
//    - It should be called exactly once.
$userRepositoryProphecy->saveUser(Argument::type('array'))
    ->willReturn(true)
    ->shouldBeCalledTimes(1);

// 4. Reveal the prophecy to get the actual mock object
$mockUserRepository = $userRepositoryProphecy->reveal();

// 5. Instantiate the UserService with the mock repository
$userService = new UserService($mockUserRepository);

// --- Test Scenario 1: Get User Details (expected success) ---
echo "Scenario 1: Getting user details (expecting success)\n";
$userDetails = $userService->getUserDetails(1);

if ($userDetails !== null && $userDetails['id'] === 1 && $userDetails['full_name'] === 'John Doe') {
    echo "  SUCCESS: User details fetched correctly.\n";
    print_r($userDetails);
} else {
    echo "  FAILURE: User details not as expected.\n";
    print_r($userDetails);
}

// --- Test Scenario 2: Create User (expected success) ---
echo "\nScenario 2: Creating a new user\n";
$newUserData = ['username' => 'johndoe', 'password' => 'secret'];
$userCreated = $userService->createUser($newUserData);

if ($userCreated) {
    echo "  SUCCESS: User created successfully.\n";
} else {
    echo "  FAILURE: User creation failed.\n";
}

// 6. Check if all expectations were met.
//    In a real phpspec/PHPUnit test, this is usually handled automatically by the framework
//    or assertion library like phpspec's `->shouldHaveBeenCalled()` or PHPUnit's `->expect()`.
//    For this standalone example, we manually "check" the prophet:
try {
    $prophet->checkPredictions();
    echo "\nAll prophecy predictions for initial scenarios met successfully!\n";
} catch (Prophecy\Exception\Prediction\PredictionException $e) {
    echo "\nProphecy Prediction Failure for initial scenarios: " . $e->getMessage() . "\n";
}

// --- Test Scenario 3: Get User Details (expected not found) ---
echo "\nScenario 3: Getting user details (expecting not found)\n";
$prophet2 = new Prophet(); // Create a new Prophet for a distinct test context
$userRepositoryProphecy2 = $prophet2->prophesize(UserRepository::class);

// Define behavior: when called with ID 2, it should return null.
// It should be called exactly once.
$userRepositoryProphecy2->findUserById(2)
    ->willReturn(null)
    ->shouldBeCalledTimes(1);

$mockUserRepository2 = $userRepositoryProphecy2->reveal();
$userService2 = new UserService($mockUserRepository2);

$userDetails2 = $userService2->getUserDetails(2);

if ($userDetails2 === null) {
    echo "  SUCCESS: User not found as expected.\n";
} else {
    echo "  FAILURE: User found unexpectedly.\n";
    print_r($userDetails2);
}

try {
    $prophet2->checkPredictions();
    echo "All prophecy predictions for scenario 3 met successfully!\n";
} catch (Prophecy\Exception\Prediction\PredictionException $e) {
    echo "Prophecy Prediction Failure for scenario 3: " . $e->getMessage() . "\n";
}

?>