12

Is there a way to mock/override an inbuilt function shell_exec in PHPUnit. I am aware of Mockery and I cannot use other libraries apart from PHPUnit.

I have tried for more than 3 hours and somewhere stuck. Any pointers / links would be highly appreciated.

I am using Zend-Framework 2.

hakre
  • 193,403
  • 52
  • 435
  • 836
notnotundefined
  • 3,493
  • 3
  • 30
  • 39

5 Answers5

12

There are several options. You could for example redeclare the php function shell_exec in the namespace of your test scope.

Check for reference this great blog post: PHP: “Mocking” built-in functions like time() in Unit Tests.

<php
namespace My\Namespace;

/**
 * Override shell_exec() in current namespace for testing
 *
 * @return int
 */
function shell_exec()
{
    return // return your mock or whatever value you want to use for testing
}
 
class SomeClassTest extends \PHPUnit_Framework_TestCase
{ 
    /*
     * Test cases
     */
    public function testSomething()
    {
        shell_exec(); // returns your custom value only in this namespace
        //...
    }
}

Now if you used the global shell_exec function inside a class in the My\Namespace it will use your custom shell_exec function instead.


You can also put the mocked function in another file (with the same namespace as the SUT) and include it in the test. Like that you can also mock the function if the test has a different namespace.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
Wilt
  • 41,477
  • 12
  • 152
  • 203
  • 3
    Your approach works only when your test script and the actual script are in the same namespace.. Mine is in separate namespace. – notnotundefined Sep 27 '16 at 11:24
  • For this to work the namespace of your test should match the namespace of the SUT (system under test). This is a common approach. Read also [this question](http://stackoverflow.com/q/8313283/1697459) or [this question](http://stackoverflow.com/q/12117254/1697459) for reference. – Wilt Sep 28 '16 at 07:19
  • nice approach ;) – hassan Aug 15 '17 at 10:06
  • You can easily put the mocked functions in another file (with the same namespace as the SUT) which is then included by the test... to use this method even when your test itself is under a different namespace. Maybe you can to add that to your answer @Wilt ...? – Mikk3lRo Oct 29 '18 at 05:53
12

An answer for non-homogeneous namespaces;

As @notnotundefined pointed out, the solution here depends on the tests living in the same namespace as the code being tested. The following is how to accomplish the same test with competing namespaces.

<?php

namespace My\Namespace {
    /**
     * Override shell_exec() in the My\Namespace namespace when testing
     *
     * @return int
     */
    function shell_exec()
    {
        return // return your mock or whatever value you want to use for testing
    }
}

namespace My\Namespace\Tests {
    class SomeClassTest extends \PHPUnit_Framework_TestCase
    {
        public function testSomething()
        {
            // The above override will be used when calling shell_exec 
            // from My\Namespace\SomeClass::something() because the 
            // current namespace is searched before the global.
            // https://www.php.net/manual/en/language.namespaces.fallback.php
            (new SomeClass())->something();
        }
    }
}
Alex Barker
  • 4,316
  • 4
  • 28
  • 47
1

You don't need any libraries or frameworks for this.

And of course, you don't want to put crutches under the code that was not designed to be testable. What you want to do is to fix the flawed architecture and make it testable.

  1. Define a ShellExecutorInterface.
  2. Inject the interface into the client classes.
  3. Provide an implementation with shell_exec() in the production environment.
  4. Provide a mock implementation in the test environment.
Kolyunya
  • 5,973
  • 7
  • 46
  • 81
  • What do you mean by `Inject`? In constructor or client class? I was trying by plain `implements` but it didn't work since the implementation lies in the SUT and not in the `interface`. – GoharSahi Aug 23 '22 at 19:06
  • @GoharSahi by `inject` I meant to pass a reference to that interface to the client code (e.g. via constructor or method parameter). – Kolyunya Aug 24 '22 at 20:13
0

You could try to use badoo/soft-mocks package to mock any in-build function, including custom objects like Mockery. For example

\Badoo\SoftMocks::redefineFunction('strlen', '', 'return 5;');

This is really useful, especially for in-build functions which depends on external resources. For example:

  • curl_exec
  • get_dns_record
FallDi
  • 660
  • 7
  • 21
-1

This is what I use:

<?php

##############################################################################
#
# Mocking class that provides fine-grained control over the behavior of
# built-in PHP functions.
#
#    From the unit test, the tester can control the behavior of mocked
#    built-in functions.  For instance, if we want file_exists to return
#    'false' for a particular test, we can do the following:
#
#            $PHP = new MockPHPWrapper();
#            $PHP->defaultRetvals['file_exists'] =  false;
#            $classUnderTest = new SomeClass($PHP);
#
#
#
#    In this example, "SomeClass" is a class that has been modified to make
#    all calls to PHP built-in functions using the real PHPWrapper.
#    For tests, we supply a MockPHPWrapper instance, run a test, then validate
#    the results in the MockPHPWrapper instance.
#
##############################################################################
class MockPHPWrapper {
    public $defaultRetvals;
    public $call_count_array;
    public $call_param_array;
    public $retval_overrides_array;

    public function __construct() {
        $this->call_count_array = array();
        $this->call_param_array = array();
        $this->retval_overrides_array = array();
        $this->setDefaultRetvals();
    }

    ##########################################################################
    #
    # Sets the return value to be used for a single instance of an invocation
    # of a mocked method.
    #
    # $funcname - name of the modcked method
    # $invocationIndex - specifies the zero-based index of the invocation -
    #                      If $funcname is 'xyz', $invocationIndex
    #                      is 2 and $retVal is 42, then the 3rd
    #                      invocaetion of 'xyz' with return 42.
    ##########################################################################
    public function overrideReturnValue($funcname, $invocationIndex, $retVal, $isBool = false) {
        if ($isBool  && $retVal === false) {
            $retVal = new FalsePlaceholder();
        }
        if ( !array_key_exists($funcname, $this->retval_overrides_array)) {
            $this->retval_overrides_array[$funcname] = array();
        }

        $this->retval_overrides_array[$funcname][$invocationIndex] = $retVal;
    }

    private function setDefaultRetvals() {
        $this->defaultRetvals = array(
            'is_readable'=>true,
            'file_exists'=>true,
            'is_executable'=>true,
            'unlink'=>true,
            'exec'=>true,
            'is_dir'=>true,
            'dirname'=>'/some/dir',
            'mkdir'=>true,
            'curl_init'=>true,
            'curl_setopt'=>true,
            'curl_exec'=>true,
            'curl_getinfo'=>'infostring',
            'curl_close'=>null,
            'fopen'=>1,
            'filesize'=>2,
            'file_put_contents'=>3);
        // for each key in the defaultRetvals array, we will
        // add a 0 entry for the call_count_array
        foreach ($this->defaultRetvals as $key=>$value) {
            if (!array_key_exists($key, $this->call_count_array)) {
                $this->call_count_array[$key] = 0;

                // now initialize the param array element
                $this->call_param_array[$key] = array();
            }
        }

    }

    public function getLastParameter($funcname) {
        return end($this->call_param_array[$funcname]);
    }

    public function getParametersByIndex($funcname, $n) {
        return $this->call_param_array[$funcname][$n];
    }

    protected function returnValue($funcname) {
        $currentCallCount = $this->call_count_array[$funcname] - 1;

        if (array_key_exists($funcname, $this->retval_overrides_array) &&
            array_key_exists($currentCallCount, $this->retval_overrides_array[$funcname])) {
            $retval = $this->retval_overrides_array[$funcname][$currentCallCount];
            if (is_object($retval) && get_class($retval) === 'FalsePlaceholder') {
                $retval = false;
                return $retval;
            }
            return $retval;
        } else {
            $defaultRetval = $this->defaultRetvals[$funcname];
            return $defaultRetval;
        }
    }

    public function dumpCountResults() {
        return json_encode($this->call_count_array);
    }

    public function dumpDefaultRetvals() {
        return json_encode($this->defaultRetvals);
    }

    public function dumpOverrideRetvals() {
        return json_encode($this->retval_overrides_array);
    }

    # Mocks
    public function is_readable($in){
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function file_exists($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function is_executable($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function unlink($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function exec($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function is_dir($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function dirname($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function mkdir($in1, $in2, $in3) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], array($in1, $in2, $in3));
        return $this->returnValue(__FUNCTION__);
    }
    public function curl_init($in) {
        $this->call_count_array[__FUNCTION__]++;
        $this->call_param_array[__FUNCTION__] = array($in);
        return $this->returnValue(__FUNCTION__);
    }
    public function curl_setopt($in1, $in2, $in3)   {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], array($in1, $in2, $in3));
        return $this->returnValue(__FUNCTION__);
    }
    public function curl_exec($in) {
        $this->call_count_array[__FUNCTION__]++;
        $this->call_param_array[__FUNCTION__] = array($in);
        return $this->returnValue(__FUNCTION__);
    }
    public function curl_getinfo($in1, $in2) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], array($in1, $in2));
        return $this->returnValue(__FUNCTION__);
    }
    public function curl_close($in) {
        $this->call_count_array[__FUNCTION__]++;
        $this->call_param_array[__FUNCTION__] = array($in);
        return null;
    }
    public function fopen($in1, $in2) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], array($in1, $in2));
        return $this->returnValue(__FUNCTION__);
    }
    public function filesize($in) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], $in);
        return $this->returnValue(__FUNCTION__);
    }
    public function file_put_contents($in1, $in2) {
        $this->call_count_array[__FUNCTION__]++;
        array_push($this->call_param_array[__FUNCTION__], array($in1, $in2));
        return $this->returnValue(__FUNCTION__);
    }

}

class FalsePlaceholder {}
?>

The mock wrapper above is used for tests, and live code just delegates to actual built-ins through this simple wrapper:

<?php
/*
 * PHP wrapper used by some scripts
 *
 */

##############################################################################
#
# Utility wrapper for built-in PHP functions
#  (very helpful for making code unit-testable)
#
##############################################################################
class PHPWrapper {
    public function is_readable($in) {
        return is_readable($in);
    }
    public function file_exists($file) {
        return file_exists($file);
    }
    public function is_executable($in) {
        return is_executable($in);
    }
    public function unlink($in) {
        return unlink($in);
    }
    public function exec($in) {
         return exec($in);
    }
    public function is_dir($in) {
        return is_dir($in);
    }
    public function dirname($in) {
        return dirname($in);
    }
    public function mkdir($name, $perms, $recur)    {
        return mkdir($name, $perms, $recur);
    }
    public function curl_init($url)   {
        return curl_init($url);
    }
    public function curl_setopt($handle, $option, $value)   {
         return curl_setopt($handle, $option, $value);
    }
    public function curl_exec($handle) {
        return curl_exec($handle);
    }
    public function curl_getinfo($handle, $option = null) {
        return curl_getinfo($handle, $option);
    }
    public function curl_close($handle) {
        return curl_close($handle);
    }
    public function fopen($filename, $mode) {
        return fopen($filename, $mode);
    }
    public function filesize($filename) {
        return filesize($filename);
    }
    public function file_put_contents($filename, $contents) {
        return file_put_contents($filename, $contents);
    }
}
?>```