From 895e0a7cef428377590e31ead19160c09f4078b8 Mon Sep 17 00:00:00 2001 From: Andre Polykanine Date: Thu, 1 Feb 2024 00:08:58 +0100 Subject: [PATCH] Add PHP 8.1 compatibility; Add basic PHPCS (#3) * Add PHP 8.1 compatibility; Add basic PHPCS * Bring back PHP 7.4 compatibility --- .gitignore | 1 + composer.json | 11 +++- phpcs.xml.dist | 10 +++ phpunit.xml.dist | 26 ++++---- src/PHPUnit/Extension/FunctionMocker.php | 29 +++++---- .../FunctionMocker/CodeGenerator.php | 16 +++-- .../Extension/Fixtures/TestClass.php | 3 +- .../FunctionMocker/CodeGeneratorTest.php | 23 +++++-- .../Extension/FunctionMockerTest.php | 62 ++++++++++--------- .../Extension/IntegrationTest.php | 14 +++-- 10 files changed, 120 insertions(+), 75 deletions(-) create mode 100644 phpcs.xml.dist diff --git a/.gitignore b/.gitignore index 9274d28..40d24c4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ atlassian-ide-plugin.xml composer.lock build/ phpunit.xml +.phpunit.result.cache diff --git a/composer.json b/composer.json index b74341f..2a0004d 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,22 @@ { "name": "lstrojny/phpunit-function-mocker", - "description": "Allows mocking otherwise untestable PHP functions through the use of namespaces", + "description": "Allows mocking otherwise untestable PHP functions through the use of namespaces. PHP 8.1 compatible fork of the eponymous library by Lars Strojny", "license": "MIT", "require": { - "php": "~7.1" + "php": "^7.4 || ^8.0 || ^8.1" }, "require-dev": { - "phpunit/phpunit": "~7" + "phpunit/phpunit": "~9", + "squizlabs/php_codesniffer": "~3" }, "authors": [ { "name": "Lars Strojny", "email": "lstrojny@php.net" + }, + { + "name": "Andre Polykanine", + "email": "andre.polykanine@internations.org" } ], "autoload": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..3b9bb8e --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,10 @@ + + + */.git/* + */vendor/* + */src/* + */tests/* + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d7ba5c5..b230958 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,21 @@ - - - - - ./tests/ - - - - - - + colors="true" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + + + + ./tests/ + + + + + diff --git a/src/PHPUnit/Extension/FunctionMocker.php b/src/PHPUnit/Extension/FunctionMocker.php index 7141d4d..78cea05 100644 --- a/src/PHPUnit/Extension/FunctionMocker.php +++ b/src/PHPUnit/Extension/FunctionMocker.php @@ -9,22 +9,19 @@ class FunctionMocker { - /** @var TestCase */ - private $testCase; + private TestCase $testCase; + private string $namespace; - /** @var string */ - private $namespace; + /** @var string[] */ + private array $functions = []; - /** @var array */ - private $functions = array(); + /** @var string[] */ + private array $constants = []; - /** @var array */ - private $constants = []; + /** @var string[] */ + private static $mockedFunctions = []; - /** @var array */ - private static $mockedFunctions = array(); - - private function __construct(TestCase $testCase, $namespace) + private function __construct(TestCase $testCase, string $namespace) { $this->testCase = $testCase; $this->namespace = trim($namespace, '\\'); @@ -35,7 +32,7 @@ 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. + * \Foo\Bar the mock setcookie() created here will be used instead of the real function. */ public static function start(TestCase $testCase, string $namespace): self { @@ -58,6 +55,7 @@ public function mockFunction(string $function): self return $this; } + /** @param mixed $value */ public function mockConstant(string $constant, $value): self { $this->constants[trim($constant)] = $value; @@ -68,7 +66,7 @@ public function mockConstant(string $constant, $value): self public function getMock(): MockObject { $mock = $this->testCase->getMockBuilder('stdClass') - ->setMethods($this->functions) + ->addMethods($this->functions) ->setMockClassName('PHPUnit_Extension_FunctionMocker_' . bin2hex(random_bytes(16))) ->getMock(); @@ -78,6 +76,7 @@ public function getMock(): MockObject foreach ($this->functions as $function) { $fqFunction = $this->namespace . '\\' . $function; + if (in_array($fqFunction, static::$mockedFunctions, true)) { continue; } @@ -87,7 +86,7 @@ public function getMock(): MockObject } if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'])) { - $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'] = array(); + $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'] = []; } $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace] = $mock; diff --git a/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php b/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php index 62e443b..b10afe7 100644 --- a/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php +++ b/src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php @@ -32,7 +32,8 @@ public static function defineFunction(string $namespace, string $function): void eval($code); } - public static function generateConstant($namespace, $constant, $value) + /** @param mixed $value */ + public static function generateConstant(string $namespace, string $constant, $value): string { $template = <<<'EOS' namespace {namespace} @@ -40,7 +41,13 @@ public static function generateConstant($namespace, $constant, $value) 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})); + throw new \RuntimeException( + sprintf( + 'Cannot redeclare constant "{constant}" in namespace "%s". Already defined as "%s"', + __NAMESPACE__, + {value} + ) + ); } } EOS; @@ -60,15 +67,14 @@ public static function defineConstant(string $namespace, string $name, string $v eval(self::generateConstant($namespace, $name, $value)); } + /** @param string[] $parameters */ private static function renderTemplate(string $template, array $parameters): string { return strtr( $template, array_combine( array_map( - function (string $key): string { - return '{' . $key . '}'; - }, + fn(string $key): string => '{' . $key . '}', array_keys($parameters) ), array_values($parameters) diff --git a/tests/PHPUnitTests/Extension/Fixtures/TestClass.php b/tests/PHPUnitTests/Extension/Fixtures/TestClass.php index 716f130..9ff406b 100644 --- a/tests/PHPUnitTests/Extension/Fixtures/TestClass.php +++ b/tests/PHPUnitTests/Extension/Fixtures/TestClass.php @@ -3,12 +3,13 @@ class TestClass { + /** @return string|int */ public static function invokeGlobalFunction() { return strpos('ffoo', 'o'); } - public static function getGlobalConstant() + public static function getGlobalConstant(): string { return CNT; } diff --git a/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php b/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php index 9ee8fe1..a2e32f8 100644 --- a/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php +++ b/tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php @@ -6,7 +6,7 @@ class CodeGeneratorTest extends TestCase { - public function testGenerateFunctionMock() + public function testGenerateFunctionMock(): void { $code = CodeGenerator::generateFunction('Test\Namespace', 'strlen'); @@ -23,10 +23,11 @@ function strlen(...$args) } } EOS; + self::assertSame($expected, $code); } - public function testGenerateStringConstantMock() + public function testGenerateStringConstantMock(): void { $code = CodeGenerator::generateConstant('Test\Namespace', 'CONSTANT', 'value'); @@ -36,10 +37,17 @@ public function testGenerateStringConstantMock() 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')); + throw new \RuntimeException( + sprintf( + 'Cannot redeclare constant "CONSTANT" in namespace "%s". Already defined as "%s"', + __NAMESPACE__, + 'value' + ) + ); } } EOS; + self::assertSame($expected, $code); } @@ -53,10 +61,17 @@ public function testGenerateIntegerConstantMock(): void 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)); + 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 7136eb1..b749bc7 100644 --- a/tests/PHPUnitTests/Extension/FunctionMockerTest.php +++ b/tests/PHPUnitTests/Extension/FunctionMockerTest.php @@ -3,25 +3,25 @@ use PHPUnit\Extension\FunctionMocker; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionClass; class FunctionMockerTest extends TestCase { - /** @var FunctionMocker */ - private $functionMocker; + private FunctionMocker $functionMocker; - public function setUp() + public function setUp(): void { $this->functionMocker = FunctionMocker::start($this, 'My\TestNamespace'); } - public function tearDown() + public function tearDown(): void { FunctionMocker::tearDown(); } - public function testBasicMockingFunction() + public function testBasicMockingFunction(): void { $this->assertMockFunctionNotDefined('My\TestNamespace\strlen'); @@ -40,25 +40,25 @@ public function testBasicMockingFunction() $mock ->expects(self::once()) ->method('strlen') - ->will(self::returnValue('mocked strlen()')) - ; + ->will(self::returnValue('mocked strlen()')); $mock ->expects(self::once()) ->method('substr') ->will( self::returnCallback( - function() { - return func_get_args(); - } - )) + /** @return string[] */ + fn(): array => func_get_args() + ) + ) ; $this->assertMockObjectPresent('My\TestNamespace', $mock); + self::assertSame('mocked strlen()', \My\TestNamespace\strlen('foo')); - self::assertSame(array('foo', 0, 3), \My\TestNamespace\substr('foo', 0, 3)); + self::assertSame(['foo', 0, 3], \My\TestNamespace\substr('foo', 0, 3)); } - public function testNamespaceLeadingAndTrailingSlash() + public function testNamespaceLeadingAndTrailingSlash(): void { $this->functionMocker = FunctionMocker::start($this, '\My\TestNamespace\\'); @@ -76,14 +76,14 @@ public function testNamespaceLeadingAndTrailingSlash() $mock ->expects(self::once()) ->method('strpos') - ->will(self::returnArgument(1)) - ; + ->will(self::returnArgument(1)); $this->assertMockObjectPresent('My\TestNamespace', $mock); + self::assertSame('b', \My\TestNamespace\strpos('abc', 'b')); } - public function testFunctionsAreUsedLowercase() + public function testFunctionsAreUsedLowercase(): void { $this->assertMockFunctionNotDefined('My\TestNamespace\myfunc'); @@ -101,14 +101,14 @@ public function testFunctionsAreUsedLowercase() $mock ->expects(self::once()) ->method('myfunc') - ->will(self::returnArgument(0)) - ; + ->will(self::returnArgument(0)); $this->assertMockObjectPresent('My\TestNamespace', $mock); + self::assertSame('abc', \My\TestNamespace\myfunc('abc')); } - public function testUseOneFunctionMockerMoreThanOnce() + public function testUseOneFunctionMockerMoreThanOnce(): void { $this->assertMockFunctionNotDefined('My\TestNamespace\strtr'); @@ -130,8 +130,7 @@ public function testUseOneFunctionMockerMoreThanOnce() ->expects(self::once()) ->method('strtr') ->with('abcd') - ->will(self::returnArgument(0)) - ; + ->will(self::returnArgument(0)); $this->assertMockObjectPresent('My\TestNamespace', $mock); @@ -139,17 +138,17 @@ public function testUseOneFunctionMockerMoreThanOnce() self::assertSame('abc', \My\TestNamespace\strtr('abc')); self::fail('Expected exception'); } catch (AssertionFailedError $e) { - self::assertContains('does not match expected value', $e->getMessage()); + self::assertStringContainsString('does not match expected value', $e->getMessage()); } /** Reset mock objects */ $reflected = new ReflectionClass(TestCase::class); $mockObjects = $reflected->getProperty('mockObjects'); $mockObjects->setAccessible(true); - $mockObjects->setValue($this, array()); + $mockObjects->setValue($this, []); } - public function testMockSameFunctionIsDifferentNamespaces() + public function testMockSameFunctionIsDifferentNamespaces(): void { $this->assertMockFunctionNotDefined('My\TestNamespace\foofunc'); $this->functionMocker @@ -159,31 +158,36 @@ public function testMockSameFunctionIsDifferentNamespaces() $this->assertMockFunctionDefined('My\TestNamespace\foofunc', 'My\TestNamespace'); $this->functionMocker = FunctionMocker::start($this, 'My\TestNamespace2'); + self::assertFalse(function_exists('My\TestNamespace2\foofunc')); + $this->functionMocker ->mockFunction('foofunc'); - self::assertFalse(function_exists('My\TestNamespace2\foofunc')); - $this->functionMocker->getMock(); + + self::assertFalse(function_exists('My\TestNamespace2\foofunc')); + + $this->functionMocker->getMock(); $this->assertMockFunctionDefined('My\TestNamespace2\foofunc', 'My\TestNamespace2'); } - public function assertMockFunctionNotDefined($function) + public function assertMockFunctionNotDefined(string $function): void { self::assertFalse( function_exists($function), sprintf('Function "%s()" was expected to be undefined', $function) ); + self::assertArrayNotHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); } - public function assertMockFunctionDefined($function, $namespace) + public function assertMockFunctionDefined(string $function, string $namespace): void { self::assertTrue(function_exists($function)); self::assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); self::assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); } - public function assertMockObjectPresent($namespace, $mock) + public function assertMockObjectPresent(string $namespace, MockObject $mock): void { self::assertArrayHasKey('__PHPUNIT_EXTENSION_FUNCTIONMOCKER', $GLOBALS); self::assertArrayHasKey($namespace, $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']); diff --git a/tests/PHPUnitTests/Extension/IntegrationTest.php b/tests/PHPUnitTests/Extension/IntegrationTest.php index bf0da21..bf3ea52 100644 --- a/tests/PHPUnitTests/Extension/IntegrationTest.php +++ b/tests/PHPUnitTests/Extension/IntegrationTest.php @@ -2,16 +2,17 @@ namespace PHPUnitTests\Extension; use PHPUnit\Extension\FunctionMocker; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use PHPUnitTests\Extension\Fixtures\TestClass; -require_once __DIR__ . '/Fixtures/TestClass.php'; +require_once __DIR__ . '/Fixtures/TestClass.php'; class IntegrationTest extends TestCase { - private $php; + private MockObject $php; - public function setUp() + public function setUp(): void { $this->php = FunctionMocker::start($this, 'PHPUnitTests\Extension\Fixtures') ->mockFunction('strpos') @@ -19,7 +20,7 @@ public function setUp() ->getMock(); } - public function testMockFunction() + public function testMockFunction(): void { $this->php ->expects(self::once()) @@ -30,14 +31,15 @@ public function testMockFunction() self::assertSame('mocked', TestClass::invokeGlobalFunction()); } - public function testMockingGlobalFunctionAndCallingOriginalAgain() + public function testMockingGlobalFunctionAndCallingOriginalAgain(): void { $this->testMockFunction(); FunctionMocker::tearDown(); + self::assertSame(2, TestClass::invokeGlobalFunction()); } - public function testMockConstant() + public function testMockConstant(): void { self::assertSame('val', TestClass::getGlobalConstant()); }