Git Product home page Git Product logo

php-mock's Introduction

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:

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:

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. In most cases, you can ignore this restriction but if you happen to run into this issue you can call Mock::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 relies on the APD extension.

  • MockFunction is a PHPUnit extension. It uses the runkit extension.

  • UOPZ is a Zend extension which allows, among others, renaming and deletion of functions.

  • 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:

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:

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.

Create a Mock object. You can do this with the fluent API of MockBuilder:

After you have build your Mock object you have to call enable() to enable the mock in the given namespace. When you are finished with that mock you should disable it by calling disable() on the mock instance.

This example illustrates mocking of the unqualified function time() in the namespace foo:

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:

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().

Mock environments

Complex mock environments of several mocked functions can be grouped in a MockEnvironment:

SleepEnvironmentBuilder

The SleepEnvironmentBuilder builds a mock environment where sleep() and usleep() return immediatly. Furthermore they increase the amount of time in the mocked date(), time() and microtime():

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()

Spies

A Spy gives you access to the function invocations. Spy::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():

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 [email protected]. This library was inspired by Fabian Schmengler's article 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

php-mock's People

Contributors

edhgoose avatar geoffroy-aubry avatar heiglandreas avatar herndlm avatar hhovakimyan avatar joshk avatar malkusch avatar michalbundyra avatar ondram avatar peter-at-bpt avatar t2l avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-mock's Issues

Can I replace static method?

Hi, guy!

I want to replace my static method by using setName.
But I think It can't be static method, right?
If YES, I want to have some advice.
there is sample code below.
I'll wait for answer.

thanks!

    // foo\bar::doSomething() is replaced with 'Yepppp!'
    $builder = new \phpmock\MockBuilder();
    $builder->setNamespace('foo')
            ->setName('doSomething')
            ->setFunction(function () {
                return 'Yepppp!!';
            });
    $mock = $builder->build();
    $mock->enable();

Cannot mock die

I'm working on adding tests for older code that uses die() in one of its internal methods. I attempted to mock the die() call and got the following error when attempting to run the test:

1) tests\app\libraries\response\RedirectResponseTester::testRedirectResponse
ParseError: syntax error, unexpected 'die' (T_EXIT), expecting '('

/Users/mpeveler/Work/Submitty/Submitty/site/vendor/php-mock/php-mock/classes/Mock.php:188
/Users/mpeveler/Work/Submitty/Submitty/site/vendor/php-mock/php-mock/classes/Mock.php:96
/Users/mpeveler/Work/Submitty/Submitty/site/vendor/php-mock/php-mock-phpunit/classes/PHPMock.php:64

This was the test I was trying to run:

<?php

namespace tests\app\libraries;
use app\libraries\Core;
use PHPUnit\Framework\TestCase;

class CoreTester extends TestCase {
    use \phpmock\phpunit\PHPMock;

    /**
     * @runInSeparateProcess
     */
    public function testRedirectResponse() {
        $this->getFunctionMock('app\libraries', 'die')
            ->expects($this->once());
        $core = new Core();
        $core->redirect('http://example.com');
        $this->assertEquals("http://example.com", $response->url);
        $this->assertEquals(302, http_response_code());
    }
}

and the called function looked like:

<?php

namespace app\libraries;

class Core {
    public function redirect($url, $http_response_code = 302) {
        if (!$this->redirect) {
            return;
        }
        header('Location: ' . $url, true, $http_response_code);
        die();
    }
}

Add PHPUnit 7.4 compatibility

Fatal error: Declaration of phpmock\phpunit\MockObjectProxy::__phpunit_verify() must be compatible with PHPUnit_Framework_MockObject_MockObject::__phpunit_verify(bool $unsetInvocationMocker = true) in vendor/php-mock/php-mock-phpunit/classes/MockObjectProxy.php on line 17

[PHP 7.4] 1 failed test

Please add "7.4-snapshot" to travis

$ vendor/bin/phpunit -v
PHPUnit 8.1.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.4.0-dev
Configuration: /tmp/php-mock/phpunit.xml

............F..................................................  63 / 164 ( 38%)
............................................................... 126 / 164 ( 76%)
......................................                          164 / 164 (100%)

Time: 340 ms, Memory: 8.00 MB

There was 1 failure:

1) phpmock\MockDefiningOrderTest::testDefineBeforeFirstCallRestriction
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'foo'

/tmp/php-mock/tests/MockDefiningOrderTest.php:84

FAILURES!
Tests: 164, Assertions: 239, Failures: 1.

Need help to bypass Bug #68541

This is a handy library, and i'm using it to replace a old test case, which originally with namespace hack like:

namespace Test;
class ATest {}

namespace App;
function extension_loaded() {}

The test purpose is to simulate PHP extension load status, and throw Exception when failed.

For using php-mock, i create a trait like this:

trait ExtensionLoadedMockTrait
{
    /** @type bool */
    public static $extensionLoaded = true;


    /**
     * @param   string  $namespace
     * @return  Mock
     */
    public function buildExtensionLoadedMock($namespace)
    {
        $function = 'extension_loaded';
        $mockContainer = MockContainer::getInstance();
        $mock = $mockContainer->getMock($namespace, $function);

        if (is_null($mock)) {
            $mock = (new MockBuilder())
                ->setNamespace($namespace)
                ->setName($function)
                ->setFunction(function($ext) {
                    return self::$extensionLoaded &&
                        \extension_loaded($ext);
                })
                ->build();

            $mock->define();

            $mockContainer->registerMock($namespace, $function, $mock);
        }

        return $mock;
    }
}

The source code of MockContainer is omit. In test case, use

$this->buildExtensionLoadedMock('Foo\Bar')->enable();

to enable or disable mock.

This test works fine when run by PHPUnit with single file given, and with directory given, but fails when run PHPUnit without any parameter:

// Ok
phpunit path/to/test/case.php
// Ok
phpunit path/to/tests/
// Fail
phpunit

Why ?

As you mentioned about Bug #68541, the mock need to define before native PHP functions. So I make a special test case name like 'Lib\Aaa\Aaa.php', and I'm sure it will run before all other test cases, but the mock mechanism still not work, the mocked function are not triggered at all.

Run single file are successful, i think the code is correct, but failed when batch run confuse me, do you have any tips ? Thanks.

PHP: 5.5.9
PHPUnit: 4.5.0
Os: Ubuntu 14.04 Trusty

Deprecation Notice with Composer >= 1.10

I have the last php-mock version but I get this notice when composer i -o:

Deprecation Notice: Class phpmock\test\MockNamespaceTest located in ./vendor/php-mock/php-mock/tests/MockNamespaceTest.php does not comply with psr-4 autoloading standard. It will not autoload anymore in Composer v2.0. in phar:///usr/local/bin/composer/src/Composer/Autoload/ClassMapGenerator.php:201

Add define() to the MockEnvironment

I'm falling foul of something somewhere that is causing me an utter nightmare.

TLDR is that adding a define function on the MockEnvironment solved my woes. I'll submit a PR shortly.

I'm trying to test a class which works with time(). I have 5 PHP Unit tests which each test a different part of this class. If useful to know, the class is part of a Symfony2 project.

Part of two of these five tests is to sleep() for a period of several days. The sleep function is returning immediately so it's definitely working. Good stuff.

If I run the tests individually, the tests all pass and the requisite amount of time I sleep in the test is reflected in the class under test.

However, if I run the tests as a whole (I'm running them in PHPStorm), it is entirely dependent on the ordering of the tests as to whether they will sleep correctly.

I believe something is caching something and causing it to fall foul of Bug #68541. But I can't for love nor money figure out what. I've tried producing a minimal test case, but everything I produce seems to work as intended. I've tried modifying everything I can think of to make it work, but no such luck.

Anyway, I found that adding a define function to the MockEnvironment and getting that to call define on all the mocks works. So I'll submit a PR!

missing "tests" folder

Hi,

i have the Problem, that the Tests are missing if i install the version 2.1 via composer. If i look in your repository in github, it seems composer.json are the reason for that:

"archive": {
"exclude": ["/tests"]
}

Problem here is the Code in vendor/php-mock/php-mock/autoload.php where you create class alias for a class who does not exists in the project. I cant really use your package with this behaviour.

Doesn't call mocked function from a called function!

Hi all,
I'm new to PHP unit testing so please advise if I'm dong anything wrong. This is the scenario:

In our main code, there is a function (say b) that calls another function (say a). I want to test b() without having to test a() so want to make a mock of a. However in my test call when I can b() it still calls the original a, not my mock. Here is my code:

In my test file:

<?php
namespace AA\BB\CC;
use PHPUnit\Framework\TestCase;
use phpmock\MockBuilder;

class XYZ extends TestCase {
        private $a_mock;

        public function testb() {
                $a_builder    = new MockBuilder();
                $a_builder->setNamespace(__NAMESPACE__)
                        ->setName("a")
                        ->setFunction(
                            function ($str) {
                                return $str;
                            }
                        );

                $this->a_mock = $a_builder->build();
                $this->a_mock->enable();
                $result = b("Hello");
                        // NOTE: If I call a() here, then the mock version is called
                        // but if b() is called, it calls the other a() defined below
                $this->a_mock->disable();
        }
}

In our code base (I don't see any namespaces defined here):

function a($str) {
        // do something
}

function b($str) {
        return a($str);
}

PHP-Mock Logo

Find some nice logo which could be used as organization's profile picture.

Not working with die() nor exit()

Hi,

I hoped to mock die() function (or exit), seems not possible. I got that :

namespace cool;

use phpmock\\generator\\MockFunctionGenerator;

function exit()
{
    $arguments = [];

    $variadics = \\array_slice(\\func_get_args(), \\count($arguments));
    $arguments = \\array_merge($arguments, $variadics);

    return MockFunctionGenerator::call(
        \'exit\',
        \'cool\\exit\',
        $arguments
    );
}

PHP Parse error: syntax error, unexpected 'exit' (T_EXIT), expecting '(' in \vendor\php-mock\php-mock\classes\generator\MockFunctionGenerator.php(67) : eval()'d code on line 5

PHP 7 Return Types

It looks like Mockery is not working with the new PHP 7 Return Types.

If I have an method with a return type:

<?php

namespace Game\Repository;

use Game\Entity\UserInterface;

Interface UserRepositoryInterface
{
    public function createNewUser($username, $password, $email):UserInterface;
}

And I mock it: $userDummy = Mockery::mock('Game\Entity\UserInterface');

I get this error:

PHP Fatal error:  Declaration of 
Mockery_2_Game_Repository_UserRepositoryInterface::createNewUser($username, $password, $email) 
must be compatible with 
Game\Repository\UserRepositoryInterface::createNewUser($username, $password, $email): Game\Entity\UserInterface 
in ***\vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php(16) : eval()'d code on line 25                

If I take the return type off, Mockery works again. Mockery's php version says ">=5.5" so I was hoping it would work with 7 :)

Missing argument 5 for My/Space/mcrypt_decrypt()

Just starting to use this library, hoping I'm not missing something obvious.

I have a method that uses mcrypt_decrypt(). The call looks like this:

$plainTextToken = mcrypt_decrypt(
    MCRYPT_RIJNDAEL_128 , 
    $hashedKey, 
    hex2bin($token),  
    'ecb'
);

When I try to set this up using PHP-Mock, I do this:

$dummyPlainTextToken = 'foo|bar';
$mcrypt = $this->getFunctionMock(__NAMESPACE__, "mcrypt_decrypt");
$mcrypt->expects($this->once())->willReturn($dummyPlainTextToken);
...
$result = myFunc();

The error I get is:

Missing argument 5 for mcrypt_decrypt()...

The fifth argument ($iv) is optional, and the function works outside of the test, so I'm not sure why it's required.

Searching new maintainer

This project and (and those related towards it) searches for a new maintainer. Please volunteer.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.