Hallo alle zusammen!
Mein Name ist Anton und jetzt (nicht so lange, ungefĂ€hr ein Jahr) entwickle ich in PHP in einem groĂen und alten Projekt. Um die QualitĂ€t des Projekts sicherzustellen, verwenden wir Autotests im PHPUnit-Framework. Leider ist es so gekommen, dass die meisten unserer Autotests funktionsfĂ€hig sind. KĂŒrzlich haben wir festgestellt, dass, wenn wir Autotests weiterhin auf die gleiche Weise verwenden, die Zeit, die fĂŒr die Lösung von Problemen mit der NichtĂŒbergabe an CI aufgewendet wird, bald lĂ€nger wird als die Zeit, die fĂŒr das Schreiben von nĂŒtzlichem Code aufgewendet wird (eigentlich nicht, aber es wĂ€chst und es nicht schön).
FĂŒr eine systematische Lösung dieses Problems haben wir einen Ansatz gewĂ€hlt, bei dem viele Komponententests und eine Mindestanzahl von Funktionstests geschrieben werden. Wir haben auĂerdem beschlossen, im Rahmen der gemÀà der Scout-Regel erforderlichen Ănderungen am vorhandenen Code vorhandene Tests zu aktualisieren. FĂŒr eine Weile - die ganze Zeit haben wir neue Funktionen gesehen - war alles in Ordnung mit uns. Aber kĂŒrzlich flog eine Aufgabe zu mir, um einen Fehler im alten Code zu beheben. Ich fing an, Unit-Tests zu schreiben und stellte fest, dass das alte böse Erbe aufwachte. Es wurde klar, dass dies der Weg ins Nirgendwo ist:
- Ich habe mehrere Stunden lang ein paar TestfÀlle geschrieben;
- Mir wurde klar, dass ich in dieser Zeit einen sehr, sehr vielen sich wiederholenden Code geschrieben habe;
- hat eigentlich keine nĂŒtzliche Arbeit geleistet;
- Im Falle einer Ănderung von DI mĂŒssen Sie viel Zeit fĂŒr die nutzlose Anpassung des Testcodes aufwenden.
, . â ,
â , â .
<?php declare(strict_types=1);
namespace Company\Bundle\Tests\Unit\Service;
use PHPUnit_Framework_MockObject_MockObject;
class TestedServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
public function test_getData_DataDescripion_ResultDescription(): void
{
$this
->diConstParam
->set("value")
;
$this
->dependencyRepository
->expects($this->once())
->method('findData')
->with(10, 10)
->will($this->returnValue([]))
;
$clazz = $this->getTestedClass();
$res = $clazz->getData();
}
}
. , -. , , - .
, PHPUnit 3.x. , 2014(!) , , , .
, â , , . , â (, , 20) -, , , unit- , .
, , « » :
- setUp.
- tearDown. , ,
- -.
- . , . â . . - , . âŠ
ErwÀgen Sie, Codefragmente zu wiederholen.
Scheingenerierung
Doppelter Code-Ausschnitt:$this->service = $this
->getMockBuilder(Service::class)
->disableOriginalConstructor()
->getMock()
;
PHP hat einen guten Kandidaten, um diese monotone Arbeit monoton zu verstecken - eine magische Funktion:
__erhalten
Aber wir mĂŒssen immer noch den Typ vermitteln, den wir benetzen. Und dann wurde ein geeignetes Tool gefunden - phpDoc:
@property Typ $ service
In unserem Fall erwies es sich insbesondere unter BerĂŒcksichtigung der Funktionen moderner IDEs als am bequemsten, sie in folgendem Format einzustellen:@property \ FQCN \ Service | \ PHPUnit_Framework_MockObject_MockObject $ service
Innerhalb der __get-Funktion der abstrakten Klasse Intellegence_PHPUnit_Framework_TestCase findet eine Metaprogrammierung statt, die phpDoc analysiert und auf ihrer Basis Mocks bildet, die in der Testklasse als $ this-> propertyName verfĂŒgbar sind. Achtung, der gesamte im Artikel angegebene Code ist ein kleiner Pseudocode. Die Vollversion befindet sich im Repository:
abstract class Intelligent_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase {
public function __get(string $name)
{
$property = $this->getPropertyBag()->get($name);
if ($property === false) {
throw new Exception("Invalid property name: ${name}");
}
return $property;
}
private function getPropertyBag(): PropertyBag
{
if ($this->propertyBag === null) {
$this->propertyBag = (new DocCommentParser())
->parse(
get_class($this),
function (string $className): PHPUnit_Framework_MockObject_MockObject {
return $this->createDefaultObjectMock($className);
}
);
}
return $this->propertyBag;
}
}
, phpDoc . :
class DocCommentParser
{
public function parse(
string $className,
callable $mockCreator
): PropertyBag {
$r = new ReflectionClass($className);
$docComment = $r->getDocComment();
if ($docComment === false) {
return new PropertyBag([]);
}
return $this->parseDocString($docComment, $mockCreator);
}
private function parseDocString(
string $docComment,
callable $mockCreator
): PropertyBag {
$map = [];
$constructorParams = [];
preg_match_all('/@property\s*(?<mock>(\S*)\|(\S+)\s*\$(\S+))/', $docComment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (array_key_exists('mock', $match) && !empty($match['mock'])) {
if ($this->isMockObject($match[2])) {
$map[$match[4]] = $mockCreator($match[3]);
continue;
}
if ($this->isMockObject($match[3])) {
$map[$match[4]] = $mockCreator($match[2]);
continue;
}
}
}
return new PropertyBag($map);
}
}
? , â FQCN phpDoc property . â , .
-class ServiceTest extends PHPUnit_Framework_TestCase
{
private $repository;
protected function setUp()
{
parent::setUp();
$this
->repository = $this->getMockBuilder(DependencyRepository::class)
->disableOriginalConstructor()
->getMock()
;
}
protected function tearDown()
{
$this->repository = null;
parent::tearDown();
}
public function test_test()
{
}
}
class ServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
public function test_test()
{
}
}
3-4-5 ( ) â . , , , 7 â . , 20 â -.
. ! -, , .
:
public function test_case()
{
$this->clazz = $this->createTestClass(
$dependency1,
$dependency2,
$dependency3
);
}
, . php __call, phpDoc method.
, , ?! , property! , . , property â . â , .
, :
class ServiceTest
{
public function test_case()
{
$clazz = $this->getTestedClass();
}
}
__call:
abstract class Intelligent_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase
{
private const GET_TESTED_CLASS_METHOD_NAME = 'getTestedClass';
public function __call(string $method, array $params)
{
if ($method !== self::GET_TESTED_CLASS_METHOD_NAME) {
throw new Exception("Invalid method name: ${method}");
}
if (!array_key_exists(self::GET_TESTED_CLASS_METHOD_NAME, $this->getMethodsMap())) {
throw new Exception('Method ' . self::GET_TESTED_CLASS_METHOD_NAME . ' not annotated');
}
$params = $this->getPropertyBag()->getConstructorParams();
$paramValues = [];
foreach ($params as $param) {
$property = $this->__get($param);
$paramValues[] = $property;
}
$reflection = new ReflectionClass($this->getMethodsMap()[self::GET_TESTED_CLASS_METHOD_NAME]);
return $reflection->newInstanceArgs($paramValues);
}
}
? property:
class DocCommentParser
{
public function parse(
string $className,
callable $mockCreator
): PropertyBag {
$r = new ReflectionClass($className);
$docComment = $r->getDocComment();
if ($docComment === false) {
return new PropertyBag([], []);
}
return $this->parseDocString($docComment, $mockCreator);
}
private function parseDocString(
string $docComment,
callable $mockCreator
): PropertyBag {
$map = [];
$constructorParams = [];
preg_match_all('/@property\s*(?<mock>(\S*)\|(\S+)\s*\$(\S+))/', $docComment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (array_key_exists('mock', $match) && !empty($match['mock'])) {
if ($this->isMockObject($match[2])) {
$map[$match[4]] = $mockCreator($match[3]);
$constructorParams[] = $match[4];
continue;
}
if ($this->isMockObject($match[3])) {
$map[$match[4]] = $mockCreator($match[2]);
$constructorParams[] = $match[4];
continue;
}
}
}
return new PropertyBag($map, $constructorParams);
}
}
. , . , DI - , , , â ,
-class ServiceTest extends PHPUnit_Framework_TestCase
{
private $repository;
protected function setUp()
{
parent::setUp();
$this
->repository = $this->getMockBuilder(DependencyRepository::class)
->disableOriginalConstructor()
->getMock()
;
}
protected function tearDown()
{
$this->repository = null;
parent::tearDown();
}
public function test_test()
{
$this->clazz = $this->createTestClass(repository);
}
private function createTestClass() {
return new TestedService($repository)
}
}
â . .
ConstDependencyInjectionParameter. , .