diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8d6e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.phpunit.result.cache +composer.lock +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index f0512a0..e5e32a0 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -6,7 +6,7 @@ $finder = PhpCsFixer\Finder::create() $config = new PhpCsFixer\Config(); return $config->setRules([ - '@PHP73Migration' => true, + '@PHP71Migration' => true, '@PSR12' => true ]) ->setFinder($finder) diff --git a/README.en.md b/README.en.md index b3b816b..620783e 100644 --- a/README.en.md +++ b/README.en.md @@ -5,12 +5,10 @@ This package provides tools to circumvent difficulties when testing plug-in code from customisable frameworks (e.g. shop software). -- Method bundles can be included as traits depending on the class. -- contains methods for the production code and prepared methods for own tests to validate the production code +- method bundles can be included as traits depending on the class. - `Production\IsMockable`: contains methods for mocking parent calls -- `Development\IsMockable`: contains test code for `Production\IsMockable`. -- `Development\IsTestable`: contains methods for better accessibility of protected code +- `Development\CanAccessRestricted`: contains methods for better accessibility of protected code ## Table of content @@ -19,7 +17,6 @@ This package provides tools to circumvent difficulties when testing plug-in code - [Changelog](#changelog) - [Contributing](#contributing) - [License](#license) -- [Further licences and terms of use](#further-licences-and-terms-of-use) ## Installation diff --git a/README.md b/README.md index 133a065..589649b 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,9 @@ Dieses Paket stellt Hilfstools bereit, um Schwierigkeiten beim Testen von Plugincode aus frei erweiterbaren Frameworks (z.B. Shopsoftware) zu umgehen. - Methodenbundles lassen sich als Trait klassenabhängig einbinden -- enthält Methoden für den Produktivcode und vorbereitete Methoden für eigene Tests, um den Produktivcode zu validieren - `Production\IsMockable`: enthält Methoden zum Mocken von Parentaufrufen -- `Development\IsMockable`: enthält Testcode für `Production\IsMockable` -- `Development\IsTestable`: enthält Methoden für bessere Zugänglichkeit von protected Code +- `Development\CanAccessRestricted`: enthält Methoden für bessere Zugänglichkeit von protected Code ## Inhaltsverzeichnis @@ -19,7 +17,6 @@ Dieses Paket stellt Hilfstools bereit, um Schwierigkeiten beim Testen von Plugin - [Changelog](#changelog) - [Beitragen](#beitragen) - [Lizenz](#lizenz) -- [weitere Lizenzen und Nutzungsbedingungen](#weitere-lizenzen-und-nutzungsbedingungen) ## Installation diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 0000000..3a9acbe --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,3 @@ +# Tests + +call `./vendor/bin/phpunit` for run tests \ No newline at end of file diff --git a/Tests/Unit/Development/CanAccessRestrictedTest.php b/Tests/Unit/Development/CanAccessRestrictedTest.php new file mode 100644 index 0000000..dcfad48 --- /dev/null +++ b/Tests/Unit/Development/CanAccessRestrictedTest.php @@ -0,0 +1,249 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\TestingTools\Tests\Unit\Development; + +use D3\TestingTools\Development\CanAccessRestricted; +use D3\TestingTools\Tests\Unit\Development\HelperClasses\CanAccessRestrictedClass; +use Error; +use PHPUnit\Framework\TestCase; +use ReflectionException; + +class CanAccessRestrictedTest extends TestCase +{ + use CanAccessRestricted; + + /** @var CanAccessRestrictedClass */ + public $class; + + public function setUp(): void + { + parent::setUp(); + + $this->class = new CanAccessRestrictedClass(); + } + + /** + * @test + * + * @param string $methodName + * @param bool $accessible + * + * @dataProvider canCallMethodDataProvider + */ + public function canCallMethod(string $methodName, bool $accessible): void + { + $args = $this->getRandomString(); + $expected = 'D3\TestingTools\Tests\Unit\Development\HelperClasses\CanAccessRestrictedClass::'.$methodName.'##'.$args; + + if (!$accessible) { + $this->expectException(Error::class); + } + + $this->assertSame( + $expected, + $this->class->{$methodName}($args) + ); + } + + /** + * @param string $methodName + * + * @test + * @throws ReflectionException + * @dataProvider canCallMethodDataProvider + * @covers \D3\TestingTools\Development\CanAccessRestricted::callMethod() + */ + public function canCallMethodViaReflection(string $methodName): void + { + $args = $this->getRandomString(); + $expected = 'D3\TestingTools\Tests\Unit\Development\HelperClasses\CanAccessRestrictedClass::'.$methodName.'##'.$args; + + $this->assertSame( + $expected, + $this->callMethod( + $this->class, + $methodName, + [$args] + ) + ); + } + + /** + * @return array + */ + public function canCallMethodDataProvider(): array + { + return [ + 'public method' => ['publicMethod', true], + 'protected method' => ['protectedMethod', false], + 'private method' => ['privateMethod', false], + 'final public method' => ['finalPublicMethod', true], + ]; + } + + /** + * @test + * @param string $propertyName + * @param bool $accessible + * @dataProvider canSetAndGetClassPropertiesDataProvider + */ + public function canSetAndGetClassProperties(string $propertyName, bool $accessible): void + { + $args = $this->getRandomString(); + + if (!$accessible) { + $this->expectException(Error::class); + } + + $this->class->{$propertyName} = $args; + + $this->assertSame( + $args, + $this->class->{$propertyName} + ); + } + + /** + * @test + * + * @param string $propertyName + * + * @throws ReflectionException + * @dataProvider canSetAndGetClassPropertiesDataProvider + * @covers \D3\TestingTools\Development\CanAccessRestricted::setValue() + * @covers \D3\TestingTools\Development\CanAccessRestricted::getValue() + */ + public function canSetAndGetClassPropertiesViaReflections(string $propertyName): void + { + $args = $this->getRandomString(); + + $this->setValue( + $this->class, + $propertyName, + $args + ); + + $this->assertSame( + $args, + $this->getValue( + $this->class, + $propertyName + ) + ); + } + + /** + * @return array + */ + public function canSetAndGetClassPropertiesDataProvider(): array + { + return [ + 'public property' => ['publicProperty', true], + 'protected property' => ['protectedProperty', false], + 'private property' => ['privateProperty', false], + ]; + } + + /** + * @test + * @param string $propertyName + * @param bool $accessible + * @dataProvider canSetAndGetMockedPropertiesDataProvider + */ + public function canSetAndGetMockedClassProperties(string $propertyName, bool $accessible): void + { + $this->class = $this->getMockBuilder(CanAccessRestrictedClass::class) + ->getMock(); + + $args = $this->getRandomString(); + + if (!$accessible) { + $this->expectException(Error::class); + } + + $this->class->{$propertyName} = $args; + + $this->assertSame( + $args, + $this->class->{$propertyName} + ); + } + + /** + * @test + * + * @param string $propertyName + * + * @throws ReflectionException + * @dataProvider canSetAndGetClassPropertiesDataProvider + * @covers \D3\TestingTools\Development\CanAccessRestricted::setMockedClassValue() + * @covers \D3\TestingTools\Development\CanAccessRestricted::getMockedClassValue() + */ + public function canSetAndGetClassMockedPropertiesViaReflections(string $propertyName): void + { + $mock = $this->getMockBuilder(CanAccessRestrictedClass::class) + ->getMock(); + + $args = $this->getRandomString(); + + $this->setMockedClassValue( + CanAccessRestrictedClass::class, + $mock, + $propertyName, + $args + ); + + $this->assertSame( + $args, + $this->getMockedClassValue( + CanAccessRestrictedClass::class, + $mock, + $propertyName + ) + ); + } + + /** + * @return array + */ + public function canSetAndGetMockedPropertiesDataProvider(): array + { + return [ + 'public property' => ['publicProperty', true], + 'protected property' => ['protectedProperty', false], + 'private property' => ['privateProperty', true], // because private properties not contained in mock + ]; + } + + /** + * @param int $length + * + * @return string + */ + protected function getRandomString(int $length = 20): string + { + return substr( + str_shuffle( + str_repeat( + $x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + (int) ceil($length/strlen($x)) + ) + ), + 1, + $length + ); + } +} diff --git a/Tests/Unit/Development/HelperClasses/CanAccessRestrictedClass.php b/Tests/Unit/Development/HelperClasses/CanAccessRestrictedClass.php new file mode 100644 index 0000000..ba72919 --- /dev/null +++ b/Tests/Unit/Development/HelperClasses/CanAccessRestrictedClass.php @@ -0,0 +1,68 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\TestingTools\Tests\Unit\Development\HelperClasses; + +class CanAccessRestrictedClass +{ + /** @var string */ + public $publicProperty = 'publicProperty'; + + /** @var string */ + protected $protectedProperty = 'protectedProperty'; + + /** @var string */ + private $privateProperty = 'privateProperty'; + + /** + * @param string $arg + * + * @return string + */ + public function publicMethod(string $arg): string + { + return __METHOD__.'##'.$arg; + } + + /** + * @param string $arg + * + * @return string + */ + protected function protectedMethod(string $arg): string + { + return __METHOD__.'##'.$arg; + } + + /** + * @param string $arg + * + * @return string + */ + private function privateMethod(string $arg): string + { + return __METHOD__.'##'.$arg; + } + + /** + * @param string $arg + * + * @return string + */ + final public function finalPublicMethod(string $arg): string + { + return __METHOD__.'##'.$arg; + } +} diff --git a/Tests/Unit/Production/HelperClasses/IsMockableClass.php b/Tests/Unit/Production/HelperClasses/IsMockableClass.php new file mode 100644 index 0000000..8fee6d6 --- /dev/null +++ b/Tests/Unit/Production/HelperClasses/IsMockableClass.php @@ -0,0 +1,33 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\TestingTools\Tests\Unit\Production\HelperClasses; + +use D3\TestingTools\Production\IsMockable; + +class IsMockableClass extends IsMockableParent +{ + use IsMockable; + + /** + * @param string $arg + * + * @return string + */ + public function myMethod(string $arg): string + { + return 'currentClass::myMethod##.'.$arg; + } +} diff --git a/Tests/Unit/Production/HelperClasses/IsMockableParent.php b/Tests/Unit/Production/HelperClasses/IsMockableParent.php new file mode 100644 index 0000000..d6b9f88 --- /dev/null +++ b/Tests/Unit/Production/HelperClasses/IsMockableParent.php @@ -0,0 +1,29 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\TestingTools\Tests\Unit\Production\HelperClasses; + +class IsMockableParent +{ + /** + * @param string $arg + * + * @return string + */ + public function myMethod(string $arg): string + { + return 'ParentClass::myMethod##'.$arg; + } +} diff --git a/Tests/Unit/Production/IsMockableTest.php b/Tests/Unit/Production/IsMockableTest.php new file mode 100644 index 0000000..8875a01 --- /dev/null +++ b/Tests/Unit/Production/IsMockableTest.php @@ -0,0 +1,93 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\TestingTools\Tests\Unit\Production; + +use D3\TestingTools\Development\CanAccessRestricted; +use D3\TestingTools\Production\IsMockable; +use D3\TestingTools\Tests\Unit\Production\HelperClasses\IsMockableClass; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use ReflectionException; +use RuntimeException; + +class IsMockableTest extends TestCase +{ + use CanAccessRestricted; + + /** + * @test + * @throws ReflectionException + */ + public function callMockableNoParent(): void + { + $methodName = $this->getRandomString(); + $argument = $this->getRandomString(); + + $traitMock = $this->getObjectForTrait(IsMockable::class); + + $this->expectException(RuntimeException::class); + + $this->callMethod( + $traitMock, + 'd3CallMockableParent', + [$methodName, [$argument]] + ); + } + + /** + * @test + * @throws ReflectionException + */ + public function callMockableParent(): void + { + $methodName = 'myMethod'; + $argument = $this->getRandomString(); + + /** @var MockObject $mock */ + $mock = $this->getMockBuilder(IsMockableClass::class) + ->getMock(); + // method from mocked class will never call, run method from parent class only + $mock->expects($this->never())->method($methodName); + + $this->assertSame( + 'ParentClass::myMethod##'.$argument, + $this->callMethod( + $mock, + 'd3CallMockableParent', + [$methodName, [$argument]] + ) + ); + } + + /** + * @param int $length + * + * @return string + */ + protected function getRandomString(int $length = 20): string + { + return substr( + str_shuffle( + str_repeat( + $x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + (int) ceil($length/strlen($x)) + ) + ), + 1, + $length + ); + } +} diff --git a/composer.json b/composer.json index 3133778..88ec2b3 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,9 @@ "keywords": [ "PHP Unit", "Tests", - "tools" + "tools", + "accessibility", + "mockable" ], "homepage": "https://d3data.de/", "authors": [ @@ -23,7 +25,7 @@ "MIT" ], "require": { - "php": "^7 || ^8" + "php": "^7.1 || ^8" }, "require-dev": { "phpunit/phpunit" : "^9.5", @@ -35,5 +37,10 @@ "D3\\TestingTools\\": "src", "D3\\TestingTools\\Tests\\": "Tests" } + }, + "scripts": { + "runtests": "./vendor/bin/phpunit", + "csfixer": "./vendor/bin/php-cs-fixer fix", + "phpstan": "./vendor/bin/phpstan analyse src Tests" } } \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index b6e0779..9d2fa15 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,7 @@ parameters: - scanFiles: - - IntelliSenseHelper.php - - ../../oxid-esales/oxideshop-ce/source/oxfunctions.php - - ../../oxid-esales/oxideshop-ce/source/overridablefunctions.php level: 9 phpVersion: 70100 checkMissingIterableValueType: false ignoreErrors: - - '#Psr\\Container\\ContainerExceptionInterface is not subtype of Throwable#' + - '#Property D3\\TestingTools\\Tests\\Unit\\Development\\HelperClasses\\CanAccessRestrictedClass::\$privateProperty is never read, only written.#' + - '#Method D3\\TestingTools\\Tests\\Unit\\Development\\HelperClasses\\CanAccessRestrictedClass::privateMethod\(\) is unused.#' diff --git a/phpunit.xml b/phpunit.xml index 516e222..a5acda7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,7 +13,7 @@ - + ./Tests diff --git a/src/Development/CanAccessRestricted.php b/src/Development/CanAccessRestricted.php new file mode 100644 index 0000000..6fc4628 --- /dev/null +++ b/src/Development/CanAccessRestricted.php @@ -0,0 +1,109 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\TestingTools\Development; + +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionClass; +use ReflectionException; +use ReflectionProperty; + +trait CanAccessRestricted +{ + /** + * Calls a private or protected object method. + * + * @param object $object + * @param string $methodName + * @param array $arguments + * + * @return mixed + * @throws ReflectionException + */ + public function callMethod($object, string $methodName, array $arguments = []) + { + $class = new ReflectionClass($object); + $method = $class->getMethod($methodName); + $method->setAccessible(true); + return $method->invokeArgs($object, $arguments); + } + + /** + * Sets a private or protected property in defined class instance + * + * @param object $object + * @param string $valueName + * @param mixed $value + * @throws ReflectionException + */ + public function setValue($object, string $valueName, $value): void + { + $reflection = new ReflectionClass($object); + $property = $reflection->getProperty($valueName); + $property->setAccessible(true); + $property->setValue($object, $value); + } + + /** + * get a private or protected property from defined class instance + * + * @param object $object + * @param string $valueName + * @return mixed + * @throws ReflectionException + */ + public function getValue($object, string $valueName) + { + $reflection = new ReflectionClass($object); + $property = $reflection->getProperty($valueName); + $property->setAccessible(true); + return $property->getValue($object); + } + + /** + * Sets a private or protected property in mocked class instance based on original class + * (required for e.g. final properties, which aren't contained in mock, but in original class) + * @param string $mockedClassName * FQNS of original class + * @param MockObject $object * mock object + * @param string $valueName * property name + * @param mixed $value * new property value + * + * @throws ReflectionException + */ + public function setMockedClassValue(string $mockedClassName, MockObject $object, string $valueName, $value): void + { + $property = new ReflectionProperty($mockedClassName, $valueName); + $property->setAccessible(true); + $property->setValue($object, $value); + } + + /** + * get a private or protected property from mocked class instance based on original class + * (required for e.g. final properties, which aren't contained in mock, but in original class) + * + * @param string $mockedClassName + * @param MockObject $object + * @param string $valueName + * + * @return mixed + * @throws ReflectionException + */ + public function getMockedClassValue(string $mockedClassName, MockObject $object, string $valueName) + { + $property = new ReflectionProperty($mockedClassName, $valueName); + $property->setAccessible(true); + return $property->getValue($object); + } +} diff --git a/src/Development/README.md b/src/Development/README.md new file mode 100644 index 0000000..148ac74 --- /dev/null +++ b/src/Development/README.md @@ -0,0 +1,5 @@ +## Development + +Tools for use in test code + +- `CanAccessRestricted` make protected and private members and methods accessible \ No newline at end of file diff --git a/src/Production/IsMockable.php b/src/Production/IsMockable.php new file mode 100644 index 0000000..4ca4f20 --- /dev/null +++ b/src/Production/IsMockable.php @@ -0,0 +1,40 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\TestingTools\Production; + +use RuntimeException as RuntimeExceptionAlias; + +trait IsMockable +{ + /** + * mockable wrapper for uncertain parent calls + * + * @param string $methodName + * @param array $arguments + * + * @return false|mixed + */ + protected function d3CallMockableParent(string $methodName, array $arguments = []) + { + if (get_parent_class($this)) { + /** @var callable $callable */ + $callable = [ parent::class, $methodName ]; + return call_user_func_array($callable, $arguments); + } + + throw new RuntimeExceptionAlias('Cannot use "parent" when current class scope has no parent'); + } +} diff --git a/src/Production/README.md b/src/Production/README.md new file mode 100644 index 0000000..9f7fd84 --- /dev/null +++ b/src/Production/README.md @@ -0,0 +1,5 @@ +## Production + +Tools for use in production code + +- `IsMockable` contain wrapper for parent method calls to keep them away from test run \ No newline at end of file