Skip to content

Commit ae9a480

Browse files
committed
Mock global function
1 parent 8ac029f commit ae9a480

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

src/GlobalFunctionMock.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace MintyPHP\Mocking;
4+
5+
use Exception;
6+
use PHPUnit\Framework\ExpectationFailedException;
7+
use PHPUnit\Framework\TestCase;
8+
use Throwable;
9+
10+
class GlobalFunctionMock
11+
{
12+
// Static properties
13+
/** @var array<string,GlobalFunctionMock> */
14+
public static array $mocks = [];
15+
16+
// Instance properties
17+
/** @var string */
18+
private string $affectedNamespace;
19+
/** @var TestCase */
20+
private TestCase $testCase;
21+
/** @var array<int,array{function:string,arguments:array<int,mixed>,returns:mixed,exception:?Throwable}> $expectations*/
22+
private array $expectations;
23+
24+
/**
25+
* Register a static mock for the given class name.
26+
* @param string $affectedNamespace The namespace where the global function is used and should be mocked
27+
* @param TestCase $testCase The PHPUnit test case
28+
*/
29+
public function __construct(string $affectedNamespace, TestCase $testCase)
30+
{
31+
$this->affectedNamespace = $affectedNamespace;
32+
$this->testCase = $testCase;
33+
$this->expectations = [];
34+
self::$mocks[$this->affectedNamespace] = $this;
35+
}
36+
37+
/**
38+
* Expect a with specific body (exact match).
39+
* @param string $function The function name
40+
* @param array<int,mixed> $arguments The arguments to expect
41+
* @param mixed $returns The return value if not void
42+
* @param ?Throwable $exception An optional exception to throw
43+
*/
44+
public function expect(string $function, array $arguments, mixed $returns = null, ?Throwable $exception = null): void
45+
{
46+
$namespace = $this->affectedNamespace;
47+
if (!function_exists("$namespace\\$function")) {
48+
eval("namespace $namespace { function $function() { return \\MintyPHP\\Mocking\\GlobalFunctionMock::handleFunctionCall('$namespace','$function',func_get_args()); } }");
49+
}
50+
$this->expectations[] = [
51+
'function' => $function,
52+
'arguments' => $arguments,
53+
'returns' => $returns,
54+
'exception' => $exception,
55+
];
56+
}
57+
58+
/** Assert that all expectations were met. */
59+
public function assertExpectationsMet(): void
60+
{
61+
if (!empty($this->expectations)) {
62+
$this->testCase->fail(sprintf('Not all expectations met for %s, %d remaining', $this->affectedNamespace, count($this->expectations)));
63+
}
64+
}
65+
66+
/**
67+
* Handle a static call to a mocked class.
68+
* @param string $namespace The namespace the function is called from
69+
* @param string $function The global function name that is called
70+
* @param array<int,mixed> $arguments The arguments passed to the function
71+
* @return mixed The return value
72+
* @throws Exception If no mock is registered or expectation fails
73+
* @throws ExpectationFailedException If expectation fails
74+
*/
75+
public static function handleFunctionCall(string $namespace, string $function, array $arguments): mixed
76+
{
77+
if (!isset(self::$mocks[$namespace])) {
78+
throw new Exception(sprintf('No mock registered for function: %s', $namespace));
79+
}
80+
$mock = self::$mocks[$namespace];
81+
if (empty($mock->expectations)) {
82+
$mock->testCase->fail(sprintf('No expectations left for %s', $function));
83+
}
84+
$expected = array_shift($mock->expectations);
85+
$mock->testCase->assertEquals($expected['function'], $function, 'Unexpected function called');
86+
$mock->testCase->assertEquals($expected['arguments'], $arguments, sprintf('Arguments mismatch for %s', $function));
87+
if ($expected['exception'] !== null) {
88+
throw $expected['exception'];
89+
}
90+
return $expected['returns'];
91+
}
92+
}

tests/GlobalFunctionMockTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace MintyPHP\Mocking\Tests;
4+
5+
use MintyPHP\Mocking\GlobalFunctionMock;
6+
use MintyPHP\Mocking\StaticMethodMock;
7+
use MintyPHP\Mocking\Tests\Math\Adder;
8+
use MintyPHP\Mocking\Tests\Time\StopWatch;
9+
use PHPUnit\Framework\AssertionFailedError;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class GlobalFunctionMockTest extends TestCase
13+
{
14+
public function testStopWatchStartStop(): void
15+
{
16+
// Create a static method mock for the Adder class
17+
$mock = new GlobalFunctionMock('MintyPHP\Mocking\Tests\Time', $this);
18+
// Set expectation for the microtime function
19+
$mock->expect('microtime',[true], 1763333612.602);
20+
$mock->expect('microtime',[true], 1763333614.825);
21+
// Use the StopWatch class which uses the global function
22+
$stopWatch = new StopWatch();
23+
$stopWatch->start();
24+
$result = $stopWatch->stop();
25+
// Verify the result
26+
$this->assertEquals(2223, $result);
27+
// Assert that all expectations were met
28+
$mock->assertExpectationsMet();
29+
}
30+
31+
public function testExtraExpectations(): void
32+
{
33+
// Create a static method mock for the Adder class
34+
$mock = new GlobalFunctionMock('MintyPHP\Mocking\Tests\Time', $this);
35+
// Set expectation for the microtime function
36+
$mock->expect('microtime',[true], 1763333612.602);
37+
$mock->expect('microtime',[true], 1763333614.825);
38+
$mock->expect('microtime',[true], 1763333616.288);
39+
// Use the StopWatch class which uses the global function
40+
$stopWatch = new StopWatch();
41+
$stopWatch->start();
42+
$stopWatch->stop();
43+
// Assert that all expectations were met
44+
try {
45+
$mock->assertExpectationsMet();
46+
$this->fail('Expected AssertionFailedError was not thrown.');
47+
} catch (AssertionFailedError $e) {
48+
$this->assertEquals('Not all expectations met for MintyPHP\Mocking\Tests\Time, 1 remaining', $e->getMessage());
49+
}
50+
}
51+
52+
public function testNotEnoughExpectations(): void
53+
{
54+
// Create a static method mock for the Adder class
55+
$mock = new GlobalFunctionMock('MintyPHP\Mocking\Tests\Time', $this);
56+
// Set expectation for the microtime function
57+
$mock->expect('microtime',[true], 1763333612.602);
58+
// Use the StopWatch class which uses the global function
59+
$stopWatch = new StopWatch();
60+
$stopWatch->start();
61+
try {
62+
// Call stop without expectation
63+
$stopWatch->stop();
64+
$this->fail('Expected AssertionFailedError was not thrown.');
65+
} catch (AssertionFailedError $e) {
66+
$this->assertEquals('No expectations left for microtime', $e->getMessage());
67+
}
68+
}
69+
}

tests/Math/Adder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ public static function add($a, $b):int
1111
$c = $a + $b;
1212
throw new Exception("This method should be mocked!");
1313
return $c;
14-
}
14+
}
1515
}

tests/Time/StopWatch.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace MintyPHP\Mocking\Tests\Time;
4+
5+
class StopWatch
6+
{
7+
private int $startTime;
8+
9+
public function __construct()
10+
{
11+
$this->startTime = 0;
12+
}
13+
14+
public function start():void
15+
{
16+
$this->startTime = intval(round(microtime(true)*1000));
17+
}
18+
19+
public function stop():float
20+
{
21+
$endTime = intval(round(microtime(true)*1000));
22+
return $endTime - $this->startTime;
23+
}
24+
}

0 commit comments

Comments
 (0)