Autonomie von Unit-Tests in PHPUnit

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;

/**
 * @method \Company\Bundle\Service\TestedService getTestedClass()
 * @property PHPUnit_Framework_MockObject_MockObject|\Company\AnotherBundle\Repository\DependencyRepository $dependencyRepository
 * @property ConstDependencyInjectionParameter $diConstParam
 */
class TestedServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
    /**
     * @throws \Exception
     */
    public function test_getData_DataDescripion_ResultDescription(): void
    {
        // data
        $this
            ->diConstParam
            ->set("value")
        ;

        // behaviour
        $this
            ->dependencyRepository
            ->expects($this->once())
            ->method('findData')
            ->with(10, 10)
            ->will($this->returnValue([]))
        ;

        // SUT
        $clazz = $this->getTestedClass();

        // act
        $res = $clazz->getData();

        // asserts
    }
}


. , -. , , - .



, PHPUnit 3.x. , 2014(!) , , , .



, — , , . , — (, , 20) -, , , unit- , .


, , « » :

  1. setUp.
  2. tearDown. , ,
  3. -.



- . , . — . . - , . 


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)
    {
        // propertyBag     ,        phpDoc property
        $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(
                        //    phpDoc   FQCN .   FQCN  ,      
                        get_class($this),
                        //     callback,   getMockBuilder -     unit-
                        function (string $className): PHPUnit_Framework_MockObject_MockObject {
                            return $this->createDefaultObjectMock($className);
                        }
                    );
            }

            return $this->propertyBag;
    }
}

, phpDoc . :


class DocCommentParser
{
    /**
     * @param string $className
     * @param callable $mockCreator
     * @throws Exception
     * @return PropertyBag
     */
    public function parse(
        string $className,
        callable $mockCreator
    ): PropertyBag {
        /** @var string[] $res */
        $r = new ReflectionClass($className);
        //   phpDoc
        $docComment = $r->getDocComment();

        if ($docComment === false) {
            return new PropertyBag([]);
        }

        return $this->parseDocString($docComment, $mockCreator);
    }

    private function parseDocString(
        string $docComment,
        callable $mockCreator
    ): PropertyBag {
        /** @var string[] $map */
        $map = [];
        /** @var string[] $constructorParams */
        $constructorParams = [];

        //   -  : property Type1|Type2 $propertyName
        //    mock,   DI     ,   
        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 . — , .


2
.

-


class ServiceTest extends PHPUnit_Framework_TestCase
{
    /** @var DependencyRepository|PHPUnit_Framework_MockObject_MockBuilder */
    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->repository = ...
    }
}


/** @property PHPUnit_Framework_MockObject_MockObject|\Company\AnotherBundle\Repository\DependencyRepository $repository */
class ServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
    public function test_test()
    {
         // $this->repository = ...
    }
}


3-4-5 ( ) — . , , , 7 — . , 20 — -.



. ! -, , .


:


public function test_case()
{
    // SUT
    //   , ,    DI    .
    // ,      DI.
    $this->clazz = $this->createTestClass(
        $dependency1,
        $dependency2,
        $dependency3 //, ...
    );
}

, . php __call, phpDoc method.


, , ?! , property! , . , property — . — , .


, :


/**
 * @method FQCN\TestedService getTestedClass()
 */
class ServiceTest
{
    public function test_case()
    {
         // SUT
         $clazz = $this->getTestedClass();
    }
}

__call:


abstract class Intelligent_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase
{
    private const GET_TESTED_CLASS_METHOD_NAME = 'getTestedClass';

    /**
     * @param string $method
     * @param array $params
     * @throws Exception
     * @return object
     */
    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');
        }

        //  ,  PropertyBag        property -    
        $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
{
    /**
     * @param string $className
     * @param callable $mockCreator
     * @throws Exception
     * @return PropertyBag
     */
    public function parse(
        string $className,
        callable $mockCreator
    ): PropertyBag {
        /** @var string[] $res */
        $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 {
        /** @var string[] $map */
        $map = [];
        /** @var string[] $constructorParams */
        $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
{
    /** @var DependencyRepository|PHPUnit_Framework_MockObject_MockBuilder */
    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)
    }
}


— . .

3
.


ConstDependencyInjectionParameter. , .



All Articles