diff --git a/composer.json b/composer.json index 44f6b50..b74341f 100644 --- a/composer.json +++ b/composer.json @@ -3,10 +3,10 @@ "description": "Allows mocking otherwise untestable PHP functions through the use of namespaces", "license": "MIT", "require": { - "php": "~7" + "php": "~7.1" }, "require-dev": { - "phpunit/phpunit": "~6" + "phpunit/phpunit": "~7" }, "authors": [ { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 435a1a3..d7ba5c5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,9 +3,7 @@ convertErrorsToExceptions="true" convertWarningsToExceptions="true" convertNoticesToExceptions="true" - mapTestClassNameToCoveredClassName="true" bootstrap="vendor/autoload.php" - strict="true" verbose="true" colors="true"> @@ -16,18 +14,6 @@ - - - + - - - - tests/ - vendor/ - /usr/share/php - - diff --git a/src/PHPUnit/Extension/FunctionMocker.php b/src/PHPUnit/Extension/FunctionMocker.php index 7d46ddf..7141d4d 100644 --- a/src/PHPUnit/Extension/FunctionMocker.php +++ b/src/PHPUnit/Extension/FunctionMocker.php @@ -2,7 +2,10 @@ namespace PHPUnit\Extension; use PHPUnit\Extension\FunctionMocker\CodeGenerator; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use function bin2hex; +use function random_bytes; class FunctionMocker { @@ -15,6 +18,9 @@ class FunctionMocker /** @var array */ private $functions = array(); + /** @var array */ + private $constants = []; + /** @var array */ private static $mockedFunctions = array(); @@ -30,22 +36,18 @@ private function __construct(TestCase $testCase, $namespace) * Example: PHP global namespace function setcookie() needs to be overridden in order to test * if a cookie gets set. When setcookie() is called from inside a class in the namespace * \Foo\Bar the mock setcookie() created here will be used instead to the real function. - * - * @param TestCase $testCase - * @param string $namespace - * @return FunctionMocker */ - public static function start(TestCase $testCase, $namespace) + public static function start(TestCase $testCase, string $namespace): self { return new static($testCase, $namespace); } - public static function tearDown() + public static function tearDown(): void { unset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); } - public function mockFunction($function) + public function mockFunction(string $function): self { $function = trim(strtolower($function)); @@ -56,40 +58,31 @@ public function mockFunction($function) return $this; } - public function getMock() + public function mockConstant(string $constant, $value): self + { + $this->constants[trim($constant)] = $value; + + return $this; + } + + public function getMock(): MockObject { $mock = $this->testCase->getMockBuilder('stdClass') ->setMethods($this->functions) - ->setMockClassName('PHPUnit_Extension_FunctionMocker_' . uniqid()) + ->setMockClassName('PHPUnit_Extension_FunctionMocker_' . bin2hex(random_bytes(16))) ->getMock(); + foreach ($this->constants as $constant => $value) { + CodeGenerator::defineConstant($this->namespace, $constant, $value); + } + foreach ($this->functions as $function) { $fqFunction = $this->namespace . '\\' . $function; if (in_array($fqFunction, static::$mockedFunctions, true)) { continue; } - if (!extension_loaded('runkit') || !ini_get('runkit.internal_override')) { - CodeGenerator::defineFunction($function, $this->namespace); - } elseif (!function_exists('__phpunit_function_mocker_' . $function)) { - runkit_function_rename($function, '__phpunit_function_mocker_' . $function); - error_log($function); - runkit_method_redefine( - $function, - function () use ($function) { - if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace])) { - return call_user_func_array('__phpunit_function_mocker_' . $function, func_get_args()); - } - - return call_user_func_array( - array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace], $function), - func_get_args() - ); - } - ); - var_dump(strlen("foo")); - } - + CodeGenerator::defineFunction($this->namespace, $function); static::$mockedFunctions[] = $fqFunction; } diff --git a/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php b/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php index e401178..62e443b 100644 --- a/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php +++ b/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php @@ -1,33 +1,78 @@ {function}(...$args); } } EOS; - return sprintf($template, $namespaceName, $functionName); + return self::renderTemplate($template, ['namespace' => $namespace, 'function' => $function]); } - public static function defineFunction($functionName, $namespaceName) + public static function defineFunction(string $namespace, string $function): void { - $code = static::generateCode($functionName, $namespaceName); + $code = static::generateFunction($namespace, $function); eval($code); } + + public static function generateConstant($namespace, $constant, $value) + { + $template = <<<'EOS' +namespace {namespace} +{ + if (!defined(__NAMESPACE__ . '\\{constant}')) { + define(__NAMESPACE__ . '\\{constant}', {value}); + } elseif ({constant} !== {value}) { + throw new \RuntimeException(sprintf('Cannot redeclare constant "{constant}" in namespace "%s". Already defined as "%s"', __NAMESPACE__, {value})); + } +} +EOS; + + return self::renderTemplate( + $template, + [ + 'namespace' => $namespace, + 'constant' => $constant, + 'value' => var_export($value, true), + ] + ); + } + + public static function defineConstant(string $namespace, string $name, string $value): void + { + eval(self::generateConstant($namespace, $name, $value)); + } + + private static function renderTemplate(string $template, array $parameters): string + { + return strtr( + $template, + array_combine( + array_map( + function (string $key): string { + return '{' . $key . '}'; + }, + array_keys($parameters) + ), + array_values($parameters) + ) + ); + } } diff --git a/tests/PHPUnitTests/Extension/Fixtures/TestClass.php b/tests/PHPUnitTests/Extension/Fixtures/TestClass.php index ee52c6a..716f130 100644 --- a/tests/PHPUnitTests/Extension/Fixtures/TestClass.php +++ b/tests/PHPUnitTests/Extension/Fixtures/TestClass.php @@ -7,4 +7,9 @@ public static function invokeGlobalFunction() { return strpos('ffoo', 'o'); } + + public static function getGlobalConstant() + { + return CNT; + } } diff --git a/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php b/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php index 8b01430..9ee8fe1 100644 --- a/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php +++ b/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php @@ -6,26 +6,57 @@ class CodeGeneratorTest extends TestCase { - public function testRetrieveSimpleFunctionMock() + public function testGenerateFunctionMock() { - $code = CodeGenerator::generateCode('strlen', 'Test\Namespace'); + $code = CodeGenerator::generateFunction('Test\Namespace', 'strlen'); $expected = <<<'EOS' namespace Test\Namespace { - function strlen() + function strlen(...$args) { - if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['Test\Namespace'])) { - return call_user_func_array('strlen', func_get_args()); + if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__])) { + return \strlen(...$args); } - return call_user_func_array( - array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['Test\Namespace'], 'strlen'), - func_get_args() - ); + return $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__]->strlen(...$args); } } EOS; - $this->assertEquals($expected, $code); + self::assertSame($expected, $code); + } + + public function testGenerateStringConstantMock() + { + $code = CodeGenerator::generateConstant('Test\Namespace', 'CONSTANT', 'value'); + + $expected = <<<'EOS' +namespace Test\Namespace +{ + if (!defined(__NAMESPACE__ . '\\CONSTANT')) { + define(__NAMESPACE__ . '\\CONSTANT', 'value'); + } elseif (CONSTANT !== 'value') { + throw new \RuntimeException(sprintf('Cannot redeclare constant "CONSTANT" in namespace "%s". Already defined as "%s"', __NAMESPACE__, 'value')); + } +} +EOS; + self::assertSame($expected, $code); + } + + public function testGenerateIntegerConstantMock(): void + { + $code = CodeGenerator::generateConstant('Test\Namespace', 'CONSTANT', 123); + + $expected = <<<'EOS' +namespace Test\Namespace +{ + if (!defined(__NAMESPACE__ . '\\CONSTANT')) { + define(__NAMESPACE__ . '\\CONSTANT', 123); + } elseif (CONSTANT !== 123) { + throw new \RuntimeException(sprintf('Cannot redeclare constant "CONSTANT" in namespace "%s". Already defined as "%s"', __NAMESPACE__, 123)); + } +} +EOS; + self::assertSame($expected, $code); } } diff --git a/tests/PHPUnitTests/Extension/FunctionMockerTest.php b/tests/PHPUnitTests/Extension/FunctionMockerTest.php index f68bf5d..7136eb1 100644 --- a/tests/PHPUnitTests/Extension/FunctionMockerTest.php +++ b/tests/PHPUnitTests/Extension/FunctionMockerTest.php @@ -38,14 +38,15 @@ public function testBasicMockingFunction() $this->assertMockFunctionDefined('My\TestNamespace\substr', 'My\TestNamespace'); $mock - ->expects($this->once()) + ->expects(self::once()) ->method('strlen') - ->will($this->returnValue('mocked strlen()')) + ->will(self::returnValue('mocked strlen()')) ; $mock - ->expects($this->once()) + ->expects(self::once()) ->method('substr') - ->will($this->returnCallback( + ->will( + self::returnCallback( function() { return func_get_args(); } @@ -53,8 +54,8 @@ function() { ; $this->assertMockObjectPresent('My\TestNamespace', $mock); - $this->assertSame('mocked strlen()', \My\TestNamespace\strlen('foo')); - $this->assertSame(array('foo', 0, 3), \My\TestNamespace\substr('foo', 0, 3)); + self::assertSame('mocked strlen()', \My\TestNamespace\strlen('foo')); + self::assertSame(array('foo', 0, 3), \My\TestNamespace\substr('foo', 0, 3)); } public function testNamespaceLeadingAndTrailingSlash() @@ -73,13 +74,13 @@ public function testNamespaceLeadingAndTrailingSlash() $this->assertMockFunctionDefined('My\TestNamespace\strpos', 'My\TestNamespace'); $mock - ->expects($this->once()) + ->expects(self::once()) ->method('strpos') - ->will($this->returnArgument(1)) + ->will(self::returnArgument(1)) ; $this->assertMockObjectPresent('My\TestNamespace', $mock); - $this->assertSame('b', \My\TestNamespace\strpos('abc', 'b')); + self::assertSame('b', \My\TestNamespace\strpos('abc', 'b')); } public function testFunctionsAreUsedLowercase() @@ -98,13 +99,13 @@ public function testFunctionsAreUsedLowercase() $this->assertMockFunctionDefined('My\TestNamespace\myfunc', 'My\TestNamespace'); $mock - ->expects($this->once()) + ->expects(self::once()) ->method('myfunc') - ->will($this->returnArgument(0)) + ->will(self::returnArgument(0)) ; $this->assertMockObjectPresent('My\TestNamespace', $mock); - $this->assertSame('abc', \My\TestNamespace\myfunc('abc')); + self::assertSame('abc', \My\TestNamespace\myfunc('abc')); } public function testUseOneFunctionMockerMoreThanOnce() @@ -126,19 +127,19 @@ public function testUseOneFunctionMockerMoreThanOnce() $this->assertMockFunctionDefined('My\TestNamespace\strtr', 'My\TestNamespace'); $mock - ->expects($this->once()) + ->expects(self::once()) ->method('strtr') ->with('abcd') - ->will($this->returnArgument(0)) + ->will(self::returnArgument(0)) ; $this->assertMockObjectPresent('My\TestNamespace', $mock); try { - $this->assertSame('abc', \My\TestNamespace\strtr('abc')); - $this->fail('Expected exception'); + self::assertSame('abc', \My\TestNamespace\strtr('abc')); + self::fail('Expected exception'); } catch (AssertionFailedError $e) { - $this->assertContains('does not match expected value', $e->getMessage()); + self::assertContains('does not match expected value', $e->getMessage()); } /** Reset mock objects */ @@ -158,34 +159,34 @@ public function testMockSameFunctionIsDifferentNamespaces() $this->assertMockFunctionDefined('My\TestNamespace\foofunc', 'My\TestNamespace'); $this->functionMocker = FunctionMocker::start($this, 'My\TestNamespace2'); - $this->assertFalse(function_exists('My\TestNamespace2\foofunc')); + self::assertFalse(function_exists('My\TestNamespace2\foofunc')); $this->functionMocker ->mockFunction('foofunc'); - $this->assertFalse(function_exists('My\TestNamespace2\foofunc')); + self::assertFalse(function_exists('My\TestNamespace2\foofunc')); $this->functionMocker->getMock(); $this->assertMockFunctionDefined('My\TestNamespace2\foofunc', 'My\TestNamespace2'); } public function assertMockFunctionNotDefined($function) { - $this->assertFalse( + self::assertFalse( function_exists($function), sprintf('Function "%s()" was expected to be undefined', $function) ); - $this->assertArrayNotHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); + self::assertArrayNotHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); } public function assertMockFunctionDefined($function, $namespace) { - $this->assertTrue(function_exists($function)); - $this->assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); - $this->assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); + self::assertTrue(function_exists($function)); + self::assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); + self::assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); } public function assertMockObjectPresent($namespace, $mock) { - $this->assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); - $this->assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); - $this->assertSame($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$namespace], $mock); + self::assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); + self::assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); + self::assertSame($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$namespace], $mock); } } diff --git a/tests/PHPUnitTests/Extension/IntegrationTest.php b/tests/PHPUnitTests/Extension/IntegrationTest.php index 7c9611b..bf0da21 100644 --- a/tests/PHPUnitTests/Extension/IntegrationTest.php +++ b/tests/PHPUnitTests/Extension/IntegrationTest.php @@ -3,6 +3,7 @@ use PHPUnit\Extension\FunctionMocker; use PHPUnit\Framework\TestCase; +use PHPUnitTests\Extension\Fixtures\TestClass; require_once __DIR__ . '/Fixtures/TestClass.php'; @@ -14,24 +15,30 @@ public function setUp() { $this->php = FunctionMocker::start($this, 'PHPUnitTests\Extension\Fixtures') ->mockFunction('strpos') + ->mockConstant('CNT', 'val') ->getMock(); } - public function testMocked() + public function testMockFunction() { $this->php - ->expects($this->once()) + ->expects(self::once()) ->method('strpos') ->with('ffoo', 'o') - ->will($this->returnValue('mocked')); + ->will(self::returnValue('mocked')); - $this->assertSame('mocked', \PHPUnitTests\Extension\Fixtures\TestClass::invokeGlobalFunction()); + self::assertSame('mocked', TestClass::invokeGlobalFunction()); } public function testMockingGlobalFunctionAndCallingOriginalAgain() { - $this->testMocked(); + $this->testMockFunction(); FunctionMocker::tearDown(); - $this->assertSame(2, \PHPUnitTests\Extension\Fixtures\TestClass::invokeGlobalFunction()); + self::assertSame(2, TestClass::invokeGlobalFunction()); + } + + public function testMockConstant() + { + self::assertSame('val', TestClass::getGlobalConstant()); } }