1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
|
# PHP-Mock: mocking built-in PHP functions
PHP-Mock is a testing library which mocks non deterministic built-in PHP functions like
`time()` or `rand()`. This is achieved by [PHP's namespace fallback policy](http://php.net/manual/en/language.namespaces.fallback.php):
> PHP will fall back to global functions […]
> if a namespaced function […] does not exist.
PHP-Mock uses that feature by providing the namespaced function. I.e. you have
to be in a **non global namespace** context and call the function
**unqualified**:
```php
namespace foo;
$time = time(); // This call can be mocked, a call to \time() can't.
```
## Requirements and restrictions
* Only *unqualified* function calls in a namespace context can be mocked.
E.g. a call for `time()` in the namespace `foo` is mockable,
a call for `\time()` is not.
* The mock has to be defined before the first call to the unqualified function
in the tested class. This is documented in [Bug #68541](https://bugs.php.net/bug.php?id=68541).
In most cases, you can ignore this restriction but if you happen to run into
this issue you can call [`Mock::define()`](http://php-mock.github.io/php-mock/api/class-phpmock.Mock.html#_define)
before that first call. This would define a side effectless namespaced
function which can be enabled later. Another effective
approach is running your test in an isolated process.
## Alternatives
If you can't rely on or just don't want to use the namespace fallback policy,
there are alternative techniques to mock built-in PHP functions:
* [**PHPBuiltinMock**](https://github.com/jadell/PHPBuiltinMock) relies on
the [APD](http://php.net/manual/en/book.apd.php) extension.
* [**MockFunction**](https://github.com/tcz/phpunit-mockfunction) is a PHPUnit
extension. It uses the [runkit](http://php.net/manual/en/book.runkit.php) extension.
* [**UOPZ**](https://github.com/krakjoe/uopz) is a Zend extension which
allows, among others, renaming and deletion of functions.
* [**vfsStream**](https://github.com/mikey179/vfsStream) is a stream wrapper for
a virtual file system. This will help you write tests which covers PHP
stream functions (e.g. `fread()` or `readdir()`).
# Installation
Use [Composer](https://getcomposer.org/):
```sh
composer require --dev php-mock/php-mock
```
# Usage
You don't need to learn yet another API. PHP-Mock has integrations
for these testing frameworks:
- [php-mock/php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit) - PHPUnit integration
- [php-mock/php-mock-mockery](https://github.com/php-mock/php-mock-mockery) - Mockery integration
- [php-mock/php-mock-prophecy](https://github.com/php-mock/php-mock-prophecy) - Prophecy (phpspec) integration
**Note:** If you plan to use one of the above mentioned testing frameworks you can skip
reading any further and just go to the particular integration project.
## PHP-Mock API
You find the API in the namespace
[`phpmock`](http://php-mock.github.io/php-mock/api/namespace-phpmock.html).
Create a [`Mock`](http://php-mock.github.io/php-mock/api/class-phpmock.Mock.html)
object. You can do this with the fluent API of [`MockBuilder`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html):
* [`MockBuilder::setNamespace()`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html#_setNamespace)
sets the target namespace of the mocked function.
* [`MockBuilder::setName()`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html#_setName)
sets the name of the mocked function (e.g. `time()`).
* [`MockBuilder::setFunction()`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html#_setFunction)
sets the concrete mock implementation.
* [`MockBuilder::setFunctionProvider()`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html#_setFunctionProvider)
sets, alternativly to `MockBuilder::setFunction()`, the mock implementation as a
[`FunctionProvider`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.FunctionProvider.html):
* [`FixedValueFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.FixedValueFunction.html)
is a simple implementation which returns always the same value.
* [`FixedMicrotimeFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.FixedMicrotimeFunction.html)
is a simple implementation which returns always the same microtime. This
class is different to `FixedValueFunction` as it contains a converter for
`microtime()`'s float and string format.
* [`FixedDateFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.FixedDateFunction.html)
is a simple implementation which returns always a formated date for the fixed timestamp.
* [`SleepFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.SleepFunction.html)
is a `sleep()` implementation, which doesn't halt but increases an
[`Incrementable`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.Incrementable.html)
e.g. a `time()` mock.
* [`UsleepFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.UsleepFunction.html)
is an `usleep()` implementation, which doesn't halt but increases an
`Incrementable` e.g. a `microtime()` mock.
* [`MockBuilder::build()`](http://php-mock.github.io/php-mock/api/class-phpmock.MockBuilder.html#_build)
builds a `Mock` object.
After you have build your `Mock` object you have to call [`enable()`](http://php-mock.github.io/php-mock/api/class-phpmock.Mock.html#_enable)
to enable the mock in the given namespace. When you are finished with that mock you
should disable it by calling [`disable()`](http://php-mock.github.io/php-mock/api/class-phpmock.Mock.html#_disable)
on the mock instance.
This example illustrates mocking of the unqualified function `time()` in the
namespace `foo`:
```php
namespace foo;
use phpmock\MockBuilder;
$builder = new MockBuilder();
$builder->setNamespace(__NAMESPACE__)
->setName("time")
->setFunction(
function () {
return 1417011228;
}
);
$mock = $builder->build();
// The mock is not enabled yet.
assert (time() != 1417011228);
$mock->enable();
assert (time() == 1417011228);
// The mock is disabled and PHP's built-in time() is called.
$mock->disable();
assert (time() != 1417011228);
```
Instead of setting the mock function with `MockBuilder::setFunction()` you could also
use the existing [`FixedValueFunction`](http://php-mock.github.io/php-mock/api/class-phpmock.functions.FixedValueFunction.html):
```php
namespace foo;
use phpmock\MockBuilder;
use phpmock\functions\FixedValueFunction;
$builder = new MockBuilder();
$builder->setNamespace(__NAMESPACE__)
->setName("time")
->setFunctionProvider(new FixedValueFunction(1417011228));
$mock = $builder->build();
```
It's important to note that `setNamespace()` should target the namespace where the function is called, not the namespace where it's being mocked. For example:
```
<?php
namespace App\Code;
class Subject
{
public function foo()
{
time();
}
}
```
In a test mocking this call:
```
<?php
namespace Tests\Unit;
class SubjectTest
{
public function myTest()
{
$builder = new MockBuilder();
$builder->setNamespace('\\App\\Code'); // ... etc
}
}
```
### Reset global state
An enabled mock changes global state. This will break subsequent tests if
they run code which would call the mock unintentionally. Therefore
you should always disable a mock after the test case. You will have to disable
the created mock. You could do this for all mocks by calling the
static method
[`Mock::disableAll()`](http://php-mock.github.io/php-mock/api/class-phpmock.Mock.html#_disableAll).
### Mock environments
Complex mock environments of several mocked functions can be grouped in a [`MockEnvironment`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.MockEnvironment.html):
* [`MockEnvironment::enable()`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.MockEnvironment.html#_enable)
enables all mocked functions of this environment.
* [`MockEnvironment::disable()`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.MockEnvironment.html#_disable)
disables all mocked functions of this environment.
* [`MockEnvironment::define()`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.MockEnvironment.html#_define)
defines all mocked functions of this environment.
#### SleepEnvironmentBuilder
The [`SleepEnvironmentBuilder`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.SleepEnvironmentBuilder.html)
builds a mock environment where `sleep()` and `usleep()` return immediatly.
Furthermore they increase the amount of time in the mocked `date()`, `time()` and
`microtime()`:
```php
namespace foo;
use phpmock\environment\SleepEnvironmentBuilder;
$builder = new SleepEnvironmentBuilder();
$builder->addNamespace(__NAMESPACE__)
->setTimestamp(1417011228);
$environment = $builder->build();
$environment->enable();
// This won't delay the test for 10 seconds, but increase time().
sleep(10);
assert(1417011228 + 10 == time());
```
If the mocked functions should be in different namespaces you can
add more namespaces with [`SleepEnvironmentBuilder::addNamespace()`](http://php-mock.github.io/php-mock/api/class-phpmock.environment.SleepEnvironmentBuilder.html#_addNamespace)
### Spies
A [`Spy`](http://php-mock.github.io/php-mock/api/class-phpmock.spy.Spy.html)
gives you access to the function invocations.
[`Spy::getInvocations()`](http://php-mock.github.io/php-mock/api/class-phpmock.spy.Spy.html#_getInvocations)
gives you access to the arguments and return value.
As a `Spy` is a specialization of `Mock` it behaves identically. However you
could ommit the third constructor parameter `callable $function` which
would then create a spy using the existing function.
E.g. a `new Spy(__NAMESPACE__ , "rand")` would create a spy which basically
proxies PHP's built-in `rand()`:
```php
namespace foo;
use phpmock\spy\Spy;
function bar($min, $max) {
return rand($min, $max) + 3;
}
$spy = new Spy(__NAMESPACE__, "rand");
$spy->enable();
$result = bar(1, 2);
assert ([1, 2] == $spy->getInvocations()[0]->getArguments());
assert ($result == $spy->getInvocations()[0]->getReturn() + 3);
```
# License and authors
This project is free and under the WTFPL.
Responsable for this project is Markus Malkusch markus@malkusch.de.
This library was inspired by Fabian Schmengler's article
[*PHP: “Mocking” built-in functions like time() in Unit Tests*](http://www.schmengler-se.de/en/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/).
## Donations
If you like PHP-Mock and feel generous donate a few Bitcoins here:
[1335STSwu9hST4vcMRppEPgENMHD2r1REK](bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK)
|