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();
return $config->setRules([
'@PHP73Migration' => true,
'@PHP71Migration' => true,
'@PSR12' => true
])
->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).
- 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

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

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": [
"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"
}
}

View File

@ -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.#'

View File

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