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