This commit is contained in:
Daniel Seifert 2022-11-11 13:26:07 +01:00
parent 4e13a0b79a
commit 4d5570c1bb
Signed by: DanielS
GPG Key ID: 8A7C4C6ED1915C6F
17 changed files with 653 additions and 18 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.phpunit.result.cache
composer.lock
.php-cs-fixer.cache

View File

@ -6,7 +6,7 @@ $finder = PhpCsFixer\Finder::create()
$config = new PhpCsFixer\Config(); $config = new PhpCsFixer\Config();
return $config->setRules([ return $config->setRules([
'@PHP73Migration' => true, '@PHP71Migration' => true,
'@PSR12' => true '@PSR12' => true
]) ])
->setFinder($finder) ->setFinder($finder)

View File

@ -5,12 +5,10 @@
This package provides tools to circumvent difficulties when testing plug-in code from customisable frameworks (e.g. shop software). 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. - 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
- `Production\IsMockable`: contains methods for mocking parent calls - `Production\IsMockable`: contains methods for mocking parent calls
- `Development\IsMockable`: contains test code for `Production\IsMockable`. - `Development\CanAccessRestricted`: contains methods for better accessibility of protected code
- `Development\IsTestable`: contains methods for better accessibility of protected code
## Table of content ## Table of content
@ -19,7 +17,6 @@ This package provides tools to circumvent difficulties when testing plug-in code
- [Changelog](#changelog) - [Changelog](#changelog)
- [Contributing](#contributing) - [Contributing](#contributing)
- [License](#license) - [License](#license)
- [Further licences and terms of use](#further-licences-and-terms-of-use)
## Installation ## Installation

View File

@ -6,11 +6,9 @@
Dieses Paket stellt Hilfstools bereit, um Schwierigkeiten beim Testen von Plugincode aus frei erweiterbaren Frameworks (z.B. Shopsoftware) zu umgehen. 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 - 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 - `Production\IsMockable`: enthält Methoden zum Mocken von Parentaufrufen
- `Development\IsMockable`: enthält Testcode für `Production\IsMockable` - `Development\CanAccessRestricted`: enthält Methoden für bessere Zugänglichkeit von protected Code
- `Development\IsTestable`: enthält Methoden für bessere Zugänglichkeit von protected Code
## Inhaltsverzeichnis ## Inhaltsverzeichnis
@ -19,7 +17,6 @@ Dieses Paket stellt Hilfstools bereit, um Schwierigkeiten beim Testen von Plugin
- [Changelog](#changelog) - [Changelog](#changelog)
- [Beitragen](#beitragen) - [Beitragen](#beitragen)
- [Lizenz](#lizenz) - [Lizenz](#lizenz)
- [weitere Lizenzen und Nutzungsbedingungen](#weitere-lizenzen-und-nutzungsbedingungen)
## Installation ## Installation

3
Tests/README.md Normal file
View File

@ -0,0 +1,3 @@
# Tests
call `./vendor/bin/phpunit` for run tests

View File

@ -0,0 +1,249 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @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
);
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @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;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @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;
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @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;
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @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
);
}
}

View File

@ -5,7 +5,9 @@
"keywords": [ "keywords": [
"PHP Unit", "PHP Unit",
"Tests", "Tests",
"tools" "tools",
"accessibility",
"mockable"
], ],
"homepage": "https://d3data.de/", "homepage": "https://d3data.de/",
"authors": [ "authors": [
@ -23,7 +25,7 @@
"MIT" "MIT"
], ],
"require": { "require": {
"php": "^7 || ^8" "php": "^7.1 || ^8"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit" : "^9.5", "phpunit/phpunit" : "^9.5",
@ -35,5 +37,10 @@
"D3\\TestingTools\\": "src", "D3\\TestingTools\\": "src",
"D3\\TestingTools\\Tests\\": "Tests" "D3\\TestingTools\\Tests\\": "Tests"
} }
},
"scripts": {
"runtests": "./vendor/bin/phpunit",
"csfixer": "./vendor/bin/php-cs-fixer fix",
"phpstan": "./vendor/bin/phpstan analyse src Tests"
} }
} }

View File

@ -1,10 +1,7 @@
parameters: parameters:
scanFiles:
- IntelliSenseHelper.php
- ../../oxid-esales/oxideshop-ce/source/oxfunctions.php
- ../../oxid-esales/oxideshop-ce/source/overridablefunctions.php
level: 9 level: 9
phpVersion: 70100 phpVersion: 70100
checkMissingIterableValueType: false checkMissingIterableValueType: false
ignoreErrors: 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.#'

View File

@ -13,7 +13,7 @@
<clover outputFile="build/logs/clover.xml"/> <clover outputFile="build/logs/clover.xml"/>
</report> </report>
</coverage> </coverage>
<testsuite name="myModule"> <testsuite name="testingTools">
<directory>./Tests</directory> <directory>./Tests</directory>
</testsuite> </testsuite>
<logging/> <logging/>

View File

@ -0,0 +1,109 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @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);
}
}

View File

@ -0,0 +1,5 @@
## Development
Tools for use in test code
- `CanAccessRestricted` make protected and private members and methods accessible

View File

@ -0,0 +1,40 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @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');
}
}

5
src/Production/README.md Normal file
View File

@ -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