2

I'm currently bringing a system under test that has never been under test previously (nor was really written with testing in mind). I have to edit the source code as little as humanly possible. This is a directive from on high, not my own idea. Ideally, I accomplish my aim without editing any source code at all.

A function I'm testing uses the built-in function file(). Previously, I've faked built-in functions by, in my test case, creating a new function, with the same name, in the same namespace as the function I'm testing, since PHP will search in the same namespace first.

namespace My\Function\Namespace

class MyClass
{
    public function theMethodImTesting()
    {
        file(...);
        ...
    }
}
namespace My\Function\Namespace

function file()
{
    \\ fake stuff for testing
}

namespace My\Testsuite\Namespace

class MyTestsuite
{
    ...
}

This has worked when I wanted to fake out the method for the entire test suite, but now I've encountered a case where I want to fake the function for just a single test.

Is there any way to programmatically define a function, inside a namespace?

CGriffin
  • 1,406
  • 15
  • 35
  • If this is for testing, you should be mocking. https://stackoverflow.com/questions/2665812/what-is-mocking – ceejayoz Mar 13 '19 at 14:09
  • OP tries to fake a __function__, not a class. – u_mulder Mar 13 '19 at 14:10
  • 1
    https://stackoverflow.com/questions/39341892/phpunit-mock-function – u_mulder Mar 13 '19 at 14:11
  • 1) I need to fake a _built-in function_, not a custom class. – CGriffin Mar 13 '19 at 14:22
  • 2) Ideally, I don't edit any source code while writing the tests (I didn't mention this in the question, so I've edited for clarity). – CGriffin Mar 13 '19 at 14:23
  • 1
    If you have the APD extension installed, you could use http://php.net/manual/en/function.override-function.php in your tests, but you may consider abstracting `file()` in your code to make it easier to test. – Devon Bessemer Mar 13 '19 at 14:39
  • https://stackoverflow.com/questions/2326835/redefine-built-in-php-functions see the answers to this question – blues Mar 13 '19 at 14:40

1 Answers1

3

Here's something that you could do using built-in PHP functionality.

Warning

This is a bit awkward (and might not work in every situation), therefore I wouldn't recommend this unless you can't use any of the stuff rightfully recommended in the comments (although I'm not familiar with them, so I can't be sure).

It should however do the job, despite how ugly it looks (yes, it does make use of the universally hated eval, but since it's for testing purposes, it should never deal with non-controlled input anyway).

Stuff that you only need to define once (and should be in its own file)

Now that this is out of the way, here it is. You add the following code somewhere, which defines the fake function and then all the (actual) fake functions you want (such as file), under a specific namespace:

namespace Fake\BuiltIn\Functions;

/**
 * Executes the given statements using fake built-in functions.
 *
 * @param callable $statements Statements to execute.
 * @return mixed Whatever $statements returns.
 * @throws \ReflectionException
 */
function fake(callable $statements)
{
  $function = new \ReflectionFunction($statements);

  $start_line = $function->getStartLine();
  $end_line = $function->getEndLine();
  $function_source = implode('',
    array_slice(file($function->getFileName()), $start_line - 1, $end_line - $start_line + 1));

  if (preg_match('/(?<={).*(?=})/s', $function_source, $matches)) {
    $function_body = $matches[0];
    $namespace = __NAMESPACE__;

    return eval("
      namespace $namespace;
      $function_body
    ");
  }

  throw new \RuntimeException('Failed to execute statements.');
}

// Below are all the fake functions

function strlen($string) {
  return 'fake result';
}

Usage

Then, whenever you need to call a chunk of code using the fake functions, you replace:

function myTestFunction() {
  // some code
  $length = strlen($mystring);
  // some code
}

with:

use function Fake\BuiltIn\Functions\fake;

function myTestFunction() {
  fake(function () {
    // some code
    $length = strlen($mystring);
    // some code
  });
}

In short, you just add fake function () { before the chunk and close it with } below. This requires minimal editing as requested.

Explanation

Basically, eval appears to be the only built-in way of evaluating, at runtime, a specific chunk of code in the context of a given namespace (unless you can call that chunk within its own namespace to begin with, obviously).

The fake function:

  • receives a callable (the statements to execute),
  • uses reflection to retrieve the statements' code,
  • uses evals to evaluate these statements under the fake namespace.

Demo

https://3v4l.org/LriLW

Jeto
  • 14,596
  • 2
  • 32
  • 46
  • This is an excellent solution (to an admittedly not excellent problem) and is parallel to the solution I've arrived at. Though in my case, instead of using `eval`, I'm writing a `tmpfile()` and then `require`ing it. Between the two, I'm not sure which I prefer, but yours sure is documented better than mine! – CGriffin Mar 13 '19 at 19:13
  • @CGriffin Happy to help. I gotta admit this took me a little while to figure out and was a pretty fun challenge, so take my own upvote :) – Jeto Mar 13 '19 at 19:18