The MockFactory
class allows for the dynamic creation of mock objects for classes or interfaces, which can be useful in unit testing or when you want to simulate the behavior of an object. This is achieved through method mocking, where you can override methods to return predefined results for testing purposes.
- Mock any class or interface: Create mock objects for both classes and interfaces dynamically.
- Method mocking: Override methods with custom behaviors, defined via closures.
- Supports class inheritance: The mock objects will inherit from the original class, allowing you to test behavior as if the real object was used.
- Supports interface implementation: If the target is an interface, it will generate a mock implementing the interface.
- Flexible mock behavior: Customize method returns and behaviors for specific testing scenarios.
composer require zeus/mock
use Zeus\Mock\MockFactory;
$mockFactory = new MockFactory();
// Create a mock instance of a class
$mockObject = $mockFactory->createMock(SomeClass::class);
// Mock a specific method on the class
$mockFactory->mockMethod('someMethod', fn($arg) => 'mocked result');
// Use the mock object
echo $mockObject->someMethod('test'); // Outputs 'mocked result'
Mocking an Interface
interface Logger {
public function log(string $message): void;
}
$mockFactory = new MockFactory();
// Create a mock instance of an interface
$mockLogger = $mockFactory->createMock(Logger::class);
// Mock the log method
$mockFactory->mockMethod('log', fn($message) =>print 'mocked log: ' . $message);
// Use the mock interface
echo $mockLogger->log('Test'); // Outputs 'mocked log: Test'
Mocking a Service Class with Dependencies
class DatabaseService {
private Logger $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function saveData(string $data): void {
// Simulate saving data and logging the action
$this->logger->log('Data saved: ' . $data);
}
}
$mockFactory = new MockFactory();
// Mock the Logger interface
$mockLogger = $mockFactory->createMock(Logger::class);
// Mock the saveData method
$mockFactory->mockMethod('log', fn($message) =>print 'mocked log: ' . $message);
// Create the mock DatabaseService with the mocked Logger
$mockDatabaseService = $mockFactory->createMock(DatabaseService::class,[
'logger' => $mockLogger
]);
// Use the service with mocked behavior
$mockDatabaseService->saveData('Test Data'); // Will output 'mocked log: Data saved: Test Data'
Type Hinting and instanceof Check
The MockFactory ensures that type hinting is respected in the mock classes. For example, if a method expects a specific type, the mock will follow that expectation.
class SomeService {
public function processData(array $data): int {
return count($data);
}
}
$mockFactory = new MockFactory();
// Create a mock instance of SomeService
$mockService = $mockFactory->createMock(SomeService::class);
// Mock the processData method
$mockFactory->mockMethod('processData', fn($data) => 42);
// Use the mock object
echo $mockService->processData([1, 2, 3]); // Outputs '42'
// Check the instance type
if ($mockService instanceof SomeService) {
echo "It's an instance of SomeService!";
}
Benefits of Type Hinting in Mocks
Avoid type-related issues: Ensures the mock correctly implements method signatures, avoiding errors caused by incorrect argument types.
Seamless integration: Your tests will integrate with the actual system as expected without concerns for type mismatch. Method Mocking and Custom Behavior
Method Mocking and Custom Behavior
You can mock specific methods using closures that define the behavior you need for testing.
$mockFactory->mockMethod('methodName', fn($arg1, $arg2) => 'custom result');
// Call the method
echo $mockObject->methodName($arg1, $arg2); // Outputs 'custom result'
Example Class
Here is a basic example class that demonstrates how to use the MockFactory
in different scenarios:
<?php
use Zeus\Mock\MockFactory;
interface Logger {
public function log(string $message): void;
}
class LoggerService implements Logger {
public function log(string $message): void {
echo $message;
}
}
class DatabaseService {
private Logger $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function saveData(string $data): void {
// Simulate saving data and logging the action
$this->logger->log('Data saved: ' . $data);
}
}
$mockFactory = new MockFactory();
// Create a mock Logger interface
$mockLogger = $mockFactory->createMock(Logger::class);
$mockFactory->mockMethod('log', fn($message) =>print 'mocked log: ' . $message);
// Create a mock DatabaseService with the mocked Logger
$mockDatabaseService = $mockFactory->createMock(DatabaseService::class,[
'logger'=>new LoggerService()
]);
// Use the service with mocked behavior
$mockDatabaseService->saveData('Test Data'); // Outputs 'mocked log: Data saved: Test Data'
This example class shows how to create mocks for both interfaces and concrete classes, including the use of mocked methods,
type hinting, and checking instance types using instanceof
.
class Person
{
public function getName(): string
{
return 'Dilo surucu';
}
}
function get_name(PersonInterface $person): string
{
return $person->getName();
}
$mockFactory = new MockFactory();
$mockPerson = $mockFactory->createMock(PersonInterface::class);
$mockFactory->mockMethod('getName', function () {
return 'Mocked Person';
});
echo get_name(new PersonInterface());//Dilo surucu
echo get_name($mockPerson);//Mocked Person
var_dump($mockPerson instanceof PersonInterface); //true
MockFactory is a powerful tool for mocking interfaces in PHP, allowing you to create mock instances and customize their behavior for unit testing. It ensures that mocked objects adhere to the expected type and guarantees compatibility with type hinting and instanceof
checks.
use Zeus\Mock\MockFactory;
interface PersonInterface
{
public function getName(): string;
}
$mockFactory = new MockFactory();
// Create a mock instance of PersonInterface
$personService = $mockFactory->createMock(PersonInterface::class);
// Mock the getName method
$mockFactory->mockMethod('getName', fn(): string => 'dilo surucu');
function get_person(PersonInterface $person): string
{
return $person->getName();
}
echo get_person($personService); // dilo surucu
There is a usage to see what kind of code the MockFactory class produces, this also allows you to debug it
interface MYInterface
{
public function foo():void;
}
$mockFactory = new MockFactory();
echo $mockFactory->generateCode(MYInterface::class,'CustomClassName');
/**
class CustomClassName implements MYInterface {
private object $mockFactory;
public function __construct($mockFactory) { $this->mockFactory = $mockFactory; }
public function foo():void {
if ($this->mockFactory->hasMethodMock('foo')) {
$this->mockFactory->invokeMockedMethod('foo', []);
return;
}
throw new \Zeus\Mock\MockMethodNotFoundException('Method foo is not mocked.');
}
}*/
Yes, this section allows you to mock php in-built functions that will allow you to do amazing things, for example, let's mock the sleep function in the example.
namespace Foo;
use Zeus\Mock\MockFunction;
$mockFunction=new MockFunction();
$mockFunction->add('sleep',function (int $seconds){
return "seconds: $seconds";
});
$mockFunction->add('time',function(){
return 100;
});
$mockFunction->scope();
echo sleep(10);//it'll return 'seconds: 10',it won't wait
echo time();//100
$mockFunction->endScope();
sleep(1);//it'll do wait for 1 second because out of the scope
echo time();//it'll return the real time because it's out of the scope
or short syntax, Return value can be added to mock function in two ways, type 1 and type 2.
$mockFunction = new MockFunction();
//type 1
$mockFunction->add('date','2011');
//type 2
$mockFunction->add('date',function (){
return '2011'
});
Sometimes we may want to use mock function for objects. Here is an example; we can determine the scope area with the scope method.
namespace Foo\Bar;
class Date
{
public function now(): int
{
return time();
}
}
//using Foo\Bar scope for mocking functions
namespace App;
use Foo\Bar\Date;
use Zeus\Mock\MockFunction;
$mock=new MockFunction()
$mock->add('time',fn()=>100);
$mock->scope('\\Foo\\Bar');
echo new Date()->now(); //100
$mock->endScope();
echo new Date()->now(); //not 100,its return now
This method defines functions in your mock object and overrides them with functions, like pulling a rabbit out of a hat.
namespace Foo\Bar;
class Date
{
public function now(): int
{
return time();
}
}
//using
namespace App;
use Foo\Bar\Date;
use Zeus\Mock\MockFunction;
$mock=new MockFunction()
$mock->add('time',fn()=>100);
echo $mock->runWithmock(new Date(), function (Date $date) {
return $date->now();
}); //100
without database connection, Yes, you are a little surprised. PDO object will work without database connection, do not worry.
use Zeus\Mock\MockFactory;
$mockFactory = new MockFactory();
$mockStatement = $mockFactory->createMock(PDOStatement::class);
$mockFactory->mockMethod('execute', fn($params) => true);
$mockFactory->mockMethod('fetch', fn($fetchMode) => ['id' => 1, 'name' => 'Dilo Surucu']);
$mockPdo = $mockFactory->createMock(PDO::class, [], true);//avoid connecting to the database,it won't throw errors
$mockFactory->mockMethod('prepare', fn($query) => $mockStatement);
function fetch_data(PDO $PDO): array
{
$query = $PDO->prepare('select * from users where id=1');
$query->execute([]);
return $query->fetch(PDO::FETCH_ASSOC);
}
$data = fetch_data($mockPdo);
print_r($data);//['id' => 1, 'name' => 'Dilo Surucu']
With the database connection, This part actually requires a database connection.
use Zeus\Mock\MockFactory;
$dsn = 'mysql:host=127.0.0.1;dbname=test;port=3306';
$username = 'root';
$password = 'my-secret-pw';
$options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$mockFactory = new MockFactory();
$mockStatement = $mockFactory->createMock(PDOStatement::class);
$mockFactory->mockMethod('execute', fn($params) => true);
$mockFactory->mockMethod('fetch', fn($fetchMode) => ['id' => 1, 'name' => 'Dilo Surucu']);
$mockPdo = $mockFactory->createMock(PDO::class,[
'dsn' => $dsn,
'username' => $username,
'password' => $password,
'options' => $options
]);
$mockFactory->mockMethod('prepare', fn($query) => $mockStatement);
$statement=$mockPdo->prepare('select * from users where id=1');
function fetch_data(PDO $PDO):array
{
$query=$PDO->prepare('select * from users where id=1');
$query->execute([]);
return $query->fetch(PDO::FETCH_ASSOC);
}
$data=fetch_data($mockPdo);
print_r($data);////['id' => 1, 'name' => 'Dilo Surucu']
onMockInstanceCreated
This method is triggered when the object is instantiated.
$mockMethod = new MockMethod();
$mockMethod->mockMethod('test', function () {
return 'test foo';
});
$mockMethod->mockMethod('now', function (int $a) {
return $a;
});
$mockFactory = new MockFactory($mockMethod);
$mockFactory
->onMockInstanceCreated(function (Date $date) {
echo $date->test(); //test foo
//$date->__construct(); for custom constructor
})
->createMock(Date::class, ['a' => 1]);
You can get the parameters sent to the constructor as soon as it is instanced.
$mockMethod = new MockMethod();
$mockFactory = new MockFactory($mockMethod);
$mockFactory->onMockInstanceCreated(function (Date $dateInstance, string $date) {
//$dateInstance
//$date 2022-12-12
});
$dateInstance = $mockFactory->createMock(Date::class, ['date' => '2022-12-12'], true);
In this section, I show how to use the getCallCount method to get how many times a method has been called.
$mockMethod = new MockMethod();
$mockMethod->mockMethod('getDate', '2024');
$dateInstance=MockFactory::from($mockMethod)->createMock(Date::class);
$dateInstance->getDate(12, 2012);
$dateInstance->getDate(12, 2015);
echo $mockMethod->getCallCount('getDate');//2
We can get how many times the mocked function was called, inside the scope, outside the scope and in total.
$mockFunction = new MockFunction();
$mockFunction->add('time', function () {
return 100;
});
$mockFunction->scope();
time(); //100
time(); //100
$mockFunction->getCalledCountInScope('time'); //2
$mockFunction->getCalledCountOutScope('time');//0
$mockFunction->endScope();
time(); //it returns real time not the 100
time(); //it returns real time not the 100
time(); //it returns real time not the 100
$mockFunction->getCalledCountOutScope('time'); //3
$mockFunction->getTotalCount('time'); //5
You can convert a mock function to php's original function, that is, restore it.
$mockFunction = new MockFunction();
$mockFunction->addIfNotDefined('date', '2010-01-01');
$mockFunction->scope();
echo $date->now(); //2010-01-01,its return the mock function value
$mockFunction->restoreOriginalFunction('date');
echo $date->now(); //2025-02-28, its return the real value of date function
$mockFunction->endScope();
or
$mockFunction = new MockFunction();
$mockFunction->addIfNotDefined('date', '2010-01-01');
$mockFunction->runWithMock(new Date(), function (Date $date) use ($mockFunction) {
echo $date->now(); //2010-01-01
$mockFunction->restoreOriginalFunction('date');
echo $date->now(); // it will return current date not the mock function
});
You may want mock functions to run only once. Here is the simple usage
$mockFunction = new MockFunction();
$mockFunction->once(function (MockFunction $function){
$function->add('date', '2022-01-01');
$function->add('time', 100);
});
$mockFunction->scope();
echo date('y-m-d'); //it will work and will return the 2010-01-01
echo date('y-m-d');//it will throw exception, because it will work just once
echo time(); //it will work and will return the 100
echo time(); ////it will throw exception, because it will work just once
///
$mockFunction->endScope();
echo date('y-m-d'); //2025-02-28, it will work,because it's out of the scope
echo date('y-m-d'); //2025-02-28, it will work,because it's out of the scope
echo date('y-m-d'); //2025-02-28, it will work,because it's out of the scope
echo time(); //it will work and will return the real time