php-mock/php-mock-phpunit

PHPMock::defineFunctionMock not working?

krodyrobi opened this issue · 14 comments

So I've read about the namespace bug and applied the recommended fix but it doesn't seem to work in this instance.
It did in another case but not here, even tried to run solo / in a different process but to no avail.
Added all the functions to the defineFunctionMock just to be safe even though most are not needed.

I've ran out of ideas, any thoughts on why this might not work?

<?php

namespace presslabs\minify;

use phpmock\phpunit\PHPMock;


class Test_PL_Minify extends \PHPUnit_Framework_TestCase {
  use PHPMock;

  public static function setUpBeforeClass() {
    # these are needed because of https://bugs.php.net/bug.php?id=68541
    # in short we use a call from an unaltered namespace then alter it which leads php confused
    PHPMock::defineFunctionMock( __NAMESPACE__, 'true_constant' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'defined' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'constant' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'pl_minify_page' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'minify_html' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'minify_css' );
    PHPMock::defineFunctionMock( __NAMESPACE__, 'minify_js' );
    # TODO refactor this duplication
  }

  function test_minify_page_minification() {
    $true_constant = $this->getFunctionMock( __NAMESPACE__, 'true_constant' );
    $true_constant->expects( $this->once() )
            ->with( 'PL_MINIFY_HTML' )
            ->willReturn( true );

    $input = 'random';

    $minify_html = $this->getFunctionMock( __NAMESPACE__, 'minify_html' );
    $minify_html->expects( $this->once() )
          ->with( $input )
          ->willReturn( 'mini' );


    $actual = pl_minify_page( $input );

    $this->assertSame( 'mini', $actual );
  }
}
namespace presslabs\minify;


function pl_minify_page( $buffer ) {
  error_log('before if' . serialize($buffer));
  if ( true_constant( 'PL_MINIFY_HTML' ) ) {
    error_log( "a" . serialize( $buffer ) );
                # gets here

    return minify_html( $buffer );
  }
  error_log('not modified');
  return $buffer;
}


function minify_css( $text ) {
  require_once( PL_PLUGINS_LIB . '/class-cssmin.php' );
  $compressor = new \CSSmin();
  $contents   = $compressor->run( $text );

  return $contents;
}


function minify_js( $text ) {
  require_once( PL_PLUGINS_LIB . '/class-JavaScriptPacker.php' );

  $packer   = new \JavaScriptPacker( $text, 'None', true, false );
  $contents = $packer->pack();

  return $contents;
}


function minify_html( $text ) {
  require_once( PL_PLUGINS_LIB . '/class-minify-html.php' );

  $buffer = \Minify_HTML::minify( $text, array(
    'cssMinifier'       => true_constant( 'PL_MINIFY_INLINE_CSS' ) ? __NAMESPACE__ . '\\minify_css' : null,
    'jsMinifier'        => true_constant( 'PL_MINIFY_INLINE_JS' ) ? __NAMESPACE__ . '\\minify_js' : null,
    'jsCleanComments'   => true_constant( 'PL_MINIFY_REMOVE_COMMENTS' ),
    'htmlCleanComments' => true_constant( 'PL_MINIFY_REMOVE_COMMENTS' )
  ) );

  return $buffer;
}


function true_constant( $name ) {
  return defined( $name ) and constant( $name ) === 'True';
}

If i try an call the mocked function inside the test it works.
Is there a way to check if the function I'm calling in the tested function is the actual mock? Or better yet, what could cause the mock not to be applied.

I've tried requiring files prior to the test running and it seems to mock it as well.
I'm not entirely sure the bug in PHP affects my code as the functions I'm mocking are used outside of a class scope, but reading through the comments I've reached something that could prove me wrong

namespace fred;
some_function_which_executes_builtin();
include "some_include_in_namespace_fred_which overrides_same_builtin.php";
some_function_which_executes_builtin();

Seems to have a wider scope functions / class constants etc.

Is there a way to check if the function I'm calling in the tested function is the actual mock?

Sure, use a debugger and have a look into which function it jumps.

what could cause the mock not to be applied.

Any code which uses the function previously in the same namespace. PHP is caching namespace resolution. If you're running a test suite and any test before is using said function unmocked, you're out of luck within the same process. You can confirm that by running your tests under HHVM. They might pass there, as HHVM doesn't have this bug.

@runInSeparateProcess is a very effective way to prevent such conditions. Did you run your tests with that annotation?

I've tried the annotation and the problem still persists, this makes me think that some of the imports have side-effects and call the method in question before the tests even run :|.

Unfortunately using HHVM for testing isn't optimal as production code does not run on it.

Edit: I've commented out all files except the one containing the sources for the methods in question and still the issue is present. Even went further and changed the bootstrap file to a bare minimum and still nothing.

Could you provide a SSCCE so that I could reproduce it?

ssce.zip

It is currently using the mockery extension but the issue is the same with the simple php-mock-phpunit.
The import location does not seem to matter. If you have any ideas on what I could check further I'll gladly help.

I ran it with php 5.6.29.

ssce.zip

Added some more tests that fail in some way or the other (may be related or completely new issues).
Can't seem to add multiple args1, arg2 / ret1, ret2 pairs as it dies at an eval stage.

Only the first test case works, the rest fail. Some even die and stop the others from executing which is even weirder.

@krodyrobi is it still an issue?

ssce.zip

php 7.2.1

"phpunit/phpunit": "5.7.27",
"mockery/mockery": "1.0",
"php-mock/php-mock-mockery": "1.2.0"

PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

.FF..E                                                              6 / 6 (100%)

Time: 285 ms, Memory: 4.00MB

There was 1 error:

1) minify\Test_PL_Minify::test_concat_3

Mockery\Exception\BadMethodCallException: Received Mockery_1_phpmock_integration097b6adf00e967240b711ee1b842ac1c_MockDelegateFunction::delegate(), but no expectations were specified

/usr/src/myapp/vendor/php-mock/php-mock/classes/Mock.php:124
/usr/src/myapp/vendor/php-mock/php-mock/classes/generator/MockFunctionGenerator.php:108
/usr/src/myapp/src/minify.php:22
/usr/src/myapp/tests/minify.php:97


--

There were 2 failures:

1) minify\Test_PL_Minify::test_minify_page_minification
Failed asserting that two strings are identical.

--- Expected
+++ Actual
@@ @@
-mini
+random

/usr/src/myapp/tests/minify.php:53


2) minify\Test_PL_Minify::test_minimal
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-hello hello
+test_arg test_arg
 
/usr/src/myapp/tests/minify.php:64

ERRORS!

Tests: 6, Assertions: 9, Errors: 1, Failures: 2.

@krodyrobi So...

It is possible to overload only methods which are called in the namespace and these can't be defined in that namespace.

So I've analized first failing test:

    function test_minify_page_minification() {
        $true_constant = PHPMockery::mock(__NAMESPACE__, 'true_constant');
        $true_constant->once()
            ->with('PL_MINIFY_HTML')
            ->andReturn(true);

        $input = 'random';

        $minify_html = PHPMockery::mock(__NAMESPACE__, 'minify_html');
        $minify_html->once()
            ->with($input)->andReturn('mini');

        $actual = pl_minify_page($input);

        $this->assertSame('mini', $actual);
    }

so you try to mock true_constant and minify_html which are called in pl_minify_page.
It's not possible to mock these methods because these methods in that namespace are already defined and loaded.

Exactly the same in case test_minimal, function minimal is defined and already loaded in that namespace so it's not possible to overload it.

So as the real issue I can consider only error in test test_concat_3. Need to find out more about Mockery to check how it works there, and if we can resolve it.

@krodyrobi Ok, I think I have solution for mockery:
php-mock/php-mock-mockery#9

php-mock-phpunit is fine, similar issue we have in php-mock-prophecy, but there is already PR with the solution: php-mock/php-mock-prophecy#6

That seems to be it, is there a composer version with which I can check?
Edit: nvm I noticed composer can clone a repo

So for the first part, if I got it right, because php has caching on the defined functions I need to call the mocked function from a test namespace != than the one it was defined in, but when mocking I still need to target definitions namespace.

Will check this out later and see what pops up.

So the branch in the pr works those tests pass.

But i still haven't got a clue how to mock the other functions :D.

@krodyrobi

But i still haven't got a clue how to mock the other functions :D.

What other function? What you try to do?

As I said, you can't overload function defined in the namespace, and you can't mock function called with context without namespace.

The purpose of the library is to allow mocking built-in PHP functions via overloading them in the namespace where these are called.

I'm trying to mock a function that is not builtin, and I guess is out of scope.
Thanks for finding a fix, I'll find a way for the rest.