20

I have an interesting scenario in that I need a function to be defined in order to make tests for another function. The function I want to test looks something like this:

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     * 
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

The reason I am checking for the existence of foo is because it may or may not be defined in a developer's project and the function baz relies on foo. Because of this, I only want baz to be defined if it can call foo.

The only problem is that so far it has been impossible to write tests for. I tried creating a bootstrap script in the PHPUnit configuration that would define a fake foo function and then require the Composer autoloader, but my main script still thinks foo is not defined. foo is not a Composer package and can not otherwise be required by my project. Obviously Mockery will not work for this either. My question is if anyone more experienced with PHPUnit has come across this issue and found a solution.

Thanks!

samrap
  • 5,595
  • 5
  • 31
  • 56
  • 1
    How about namespacing your functions for tests? I'm not 100% on what your implementation of the testing functions is, but: http://stackoverflow.com/a/12128017/2119863 – Unamata Sanatarai Sep 14 '16 at 12:48
  • The issue is that I need to define a function that does not exist in order to test my function which relies on it @UnamataSanatarai . I don't need to change the implementation of an existing function. – samrap Sep 14 '16 at 20:12
  • I would give you an answer with [php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit) if I could just add a `namespace` to your example. Otherwise go for something like runkit. – Markus Malkusch Sep 14 '16 at 20:30
  • Grrr. php-mock-phpunit looks fantastic but unfortunately the function I need to mock is in the root namespace (from a WordPress plugin; yea, don't ask me why WordPress -__-) so I'll take a look at runkit – samrap Sep 14 '16 at 20:33
  • You simply cannot mock this kind of procedural code. I would either wrap this stuff in a mockable class, or use (environmental) defines like C-style header guards. –  Sep 14 '16 at 22:18
  • What do you mean by "my main script thinks `foo` is still undefined". How did your bootstrap file look? Are you 100% sure your bootstrap file was being called first. If your bootstrap file was being called, and defining functions, then there must be something special about how your unit tests are being run for the function to become undefined – Gareth Parker Sep 15 '16 at 12:31
  • @GarethParker that's exactly what I've been thinking. But the function I am mocking in the bootstrap file _is_ defined in the tests, but the code I am testing, which defines `baz` only if `foo` is defined, is still not defining `baz`. Will follow up later tonight. – samrap Sep 15 '16 at 17:13
  • The real question is: why are you using `baz` instead of `bar`? – Infiltrator Jan 16 '17 at 02:40

5 Answers5

3

Start with a slight refactor of the code to make it more testable.

function conditionalDefine($baseFunctionName, $defineFunctionName)
{
    if(function_exists($baseFunctionName) && ! function_exists($defineFunctionName))
    {
        eval("function $defineFunctionName(\$n) { return $baseFunctionName() + \$n; }");
    }
}

Then just call it like this:

conditionalDefine('foo', 'bar');

Your PHPUnit test class will contain the following tests:

public function testFunctionIsDefined()
{
    $baseName = $this->mockBaseFunction(3);
    $expectedName = uniqid('testDefinedFunc');
    conditionalDefine($baseName, $expectedName);
    $this->assertTrue(function_exists($expectedName));
    $this->assertEquals(5, $expectedName(2)); 
}
public function testFunctionIsNotDefinedBecauseItExists()
{
    $baseName = $this->mockBaseFunction(3);
    $expectedName = $this->mockBaseFunction($value = 'predefined');
    conditionalDefine($base, $expectedName);
    // these are optional, you can't override a func in PHP
    // so all that is necessary is a call to conditionalDefine and if it doesn't
    // error, you're in the clear
    $this->assertTrue(function_exists($expectedName));
    $this->assertEquals($value, $expectedName()); 
}
public function testFunctionIsNotDefinedBecauseBaseFunctionDoesNotExists()
{
    $baseName = uniqid('testBaseFunc');
    $expectedName = uniqid('testDefinedFunc');
    conditionalDefine($base, $expectedName);
    $this->assertFalse(function_exists($expectedName));     
}

protected function mockBaseFunction($returnValue)
{
    $name = uniqid('testBaseFunc');
    eval("function $name() { return '$value'; }");
    return $name;
}

That is sufficient for what you're asking. You can however, further refactor this code extracting the function generation into a code generator. That what you can write unit tests against the generator to make sure it creates the kind of function you expect.

Jeremy Giberson
  • 1,063
  • 8
  • 15
1

This should work!

use MyProject\baz;

class YourTestCase
{
    /** @var callable **/
    protected $mockFoo;

    /** @var callable **/
    protected $fakeFoo;

    public function setUp()
    {
        if (function_exists('foo')) {
            $this->mockFoo = function($foosParams) {
                foo($foosParams);
                // Extra Stuff, as needed to make the test function right.
            };
        }

        $this->fakeFoo = function($foosParams) {
            // Completely mock out foo.
        };
    }

    public function testBazWithRealFoo()
    {
        if (!$this->mockFoo) {
            $this->markTestIncomplete('This system does not have the "\Foo" function.');
        }

        $actualResults = baz($n, $this->mockFoo);
        $this->assertEquals('...', $actualResults);
    }

    public function testBazWithMyFoo()
    {
        $actualResults = baz($n, $this->fakeFoo);
        $this->assertEquals('...', $actualResults);
    }
}

Then modify your existing code:

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     * 
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

namespace MyProject
{
    function baz($bazParams, callable $foo = '\foo')
    {
        return $foo() + $bazParams;
    }
}

Then instead of calling baz($n), you need to call:

use MyProject\baz;
baz($bazParams);

It's like Dependency Injection for functions, yo ;-)

Theodore R. Smith
  • 21,848
  • 12
  • 65
  • 91
1

Is this sufficient?

to-test.php:

<?php
if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     *
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

BazTest.php:

<?php

class BazTest extends PHPUnit_Framework_TestCase {
    public function setUp()
    {
        function foo()
        {
            // Appropriate mock goes here
            return 1;
        }

        include __DIR__ . '/to-test.php';
    }

    public function testBaz()
    {
        $this->assertEquals(2, baz(1));
        $this->assertEquals(3, baz(2));
    }
}

Running the test yields:

PHPUnit 5.4.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 54 ms, Memory: 8.00MB

OK (1 test, 2 assertions)
Justin McAleer
  • 236
  • 1
  • 9
  • Unfortunately not. I am using Composer which handles file inclusion via autoloading. – samrap Sep 15 '16 at 16:52
  • maybe you should show some of your real code... what you included is all global functions, hence it wasn't clear to me that autoloading would be in play for this specific function baz. – Justin McAleer Sep 15 '16 at 18:11
1

It seems this is a situation where you need to mock visibility, which is definitely outside the realm of PHPUnit which a library of classes. So you can still use PHPUnit, but might have to step outside of it to achieve your goal.

The only way I can think of to create a mock of a function is right inside the test case, above the object extending \PHPUnit_Framework_TestCase.

function foo() {
    return 1;
}

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     *
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}


class TestBaz extends \PHPUnit_Framework_TestCase
{

    public function test_it() {

        $this->assertSame(4,baz(3));
    }


}

You might have to create two files, one with foo and one without, or four if you want to test foo/not foo and baz/not baz. This is definitely an outside-the-box option, that I wouldn't normally recommend, but in your case, it might be your best bet.

Katie
  • 2,594
  • 3
  • 23
  • 31
  • This is no different than defining it in a separate bootstrap file, which I have tried and is not working. I'm going to do some more digging tonight and update my question with more information. – samrap Sep 15 '16 at 16:56
  • That sounds good, it is hard to understand the environment you are testing. I was thinking you could then replace your example code (the if statement) with require's that would then pull in whatever you were testing. Good luck! – Katie Sep 15 '16 at 17:04
  • @samrap One more quick thing, the test case does pass with no errors, hope that helps! – Katie Sep 15 '16 at 17:10
  • 1
    Yea like I said I'm going to have to dig more tonight. Thanks for your help! I'll follow up soon – samrap Sep 15 '16 at 17:11
0

Adding it in the bootstrap file doesn't work - why?

Is it because sometimes the function baz is (A) correctly created by the system and sometimes (B) you need to mock it? Or (C) do you always need to mock it?

  • Case A: Why is the code creating a vital function sporadically on the fly?
  • Case B: A function can only be registrered once, and never unregistered or overwritten. Therefore, you either go with the mock or you don't. No mixing is allowed.
  • Case C: If you always need to mock it, and you add it to the bootstrap file, it will be defined. Regarding what you've tried, either your bootstrap file for phpunit isn't loaded correctly or you misspelled the function's name.

I'm sure you've correctly configured your phpunit bootstrapping, but for good measure, does it look anything like the following:

/tests/phpunit.xml:

<phpunit
    bootstrap="phpunit.bootstrap.php"
</phpunit>

/tests/phpunit.bootstrap.php:

<?php
require(__DIR__ . "/../bootstrap.php"); // Application startup logic; this is where the function "baz" gets defined, if it exists

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     * 
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

Don't create the function baz on the fly in your tests, e.g. in a setUp function.

Test suites in phpunit use the same bootstrapper. Therefore, if you need to test cases where function baz is defined and other cases where it is not defined (and you need to mock it), you need to split up the tests folder, e.g. in two different folders, each with their phpunit.xml and phpunit.bootstrap.php files. E.g. /tests/with-baz and /tests/mock-baz. From these two folders, run the tests separately. Just create symlinks to the phpunit in each subfolder (e.g. from /test/with-baz create ln -s ../../vendor/bin/phpunit if composer is in the root) to ensure you run the same version of phpunit in both scenarios.

The ultimate solution is, of course, to figure out where the baz function is being defined and manually include the culprit script file, if at all possible, to ensure the correct logic is being applied.

Alternative

Use phpunit's @runInSeparateProcess annotation and define the function as needed.

<?php
class SomeTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testOne()
    {
        if (false === function_exists('baz')) {
            function baz() {
                return 42;
            }
        }
        $this->assertSame(42, baz());
    }

    public function testTwo()
    {
        $this->assertFalse(function_exists('baz'));
    }
}
Kafoso
  • 534
  • 3
  • 20
  • 1
    A and B do not apply. I do not care about the functionality of `foo` just need it to return a static value (foo is not my function and so my tests do not care how it works). My bootstrap file looks identical to your example yet the function still does not appear to be defined in the code I am testing. At this point it might be an issue with the way PHPUnit works with autoloading. I need to look at `autoload-dev` later tonight and get back to you. – samrap Sep 15 '16 at 16:54
  • 1
    Okay. That does sound weird. The above `SomeTest` does pass and I also checked when using the bootstrap approach. Looking forward to hearing what you find. – Kafoso Sep 16 '16 at 06:31