Otonomi Tes Unit di PHPUnit

Halo semuanya!


Nama saya Anton dan sekarang (tidak begitu lama, sekitar satu tahun) saya mengembangkan PHP dalam satu proyek besar dan lama. Untuk memastikan kualitas proyek, kami menggunakan autotest pada kerangka PHPUnit. Namun, sayangnya, sebagian besar autotest kami berfungsi. Baru-baru ini, kami menyadari bahwa jika kami terus menggunakan autotest dengan cara yang sama, maka segera waktu yang dihabiskan untuk menyelesaikan masalah dengan tidak meneruskannya ke CI akan menjadi lebih dari waktu yang dihabiskan untuk menulis kode yang berguna (sebenarnya tidak, tetapi itu tumbuh dan tidak baik).


Untuk solusi sistematis untuk masalah ini, kami memilih pendekatan yang melibatkan penulisan banyak tes unit dan jumlah minimum tes fungsional. Kami juga memutuskan bahwa sebagai bagian dari perubahan pada kode yang ada diperlukan sesuai dengan aturan kepanduan, perbarui tes yang ada. Untuk sementara - selama ini kami melihat fitur baru - semuanya baik-baik saja dengan kami. Namun baru-baru ini, sebuah tugas terbang kepada saya untuk memperbaiki bug di kode lama. Saya mulai menulis tes unit dan menyadari bahwa warisan kejahatan kuno bangun. Menjadi jelas bahwa ini adalah jalan ke mana-mana:

  • Saya menulis beberapa test case selama beberapa jam;
  • Saya menyadari bahwa selama ini saya menulis kode berulang yang sangat, sangat banyak;
  • tidak benar-benar melakukan pekerjaan yang bermanfaat;
  • dalam hal perubahan DI, Anda harus menghabiskan banyak waktu untuk adaptasi kode tes yang tidak berguna.


, . β€” ,



β€” , β€” .


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



- . , . β€” . . - , . …

Pertimbangkan untuk mengulangi fragmen kode.


Generasi Mock


Cuplikan kode duplikat:

$this->service = $this
    ->getMockBuilder(Service::class)
    ->disableOriginalConstructor()
    ->getMock()
;

PHP memiliki kandidat yang bagus untuk menyembunyikan karya monoton yang monoton ini - fungsi ajaib:

__Dapatkan

Tapi kita masih perlu menyampaikan tipe yang kita ngompol. Dan kemudian alat yang cocok ditemukan - phpDoc:

@ properti Jenis layanan $

Dalam kasus kami, terutama dengan mempertimbangkan kemampuan IDE modern, ternyata lebih mudah untuk mengaturnya dalam format berikut:
@property \ FQCN \ Service | \ PHPUnit_Framework_MockObject_MockObject $ service

Di dalam fungsi __get dari kelas abstrak Intellegence_PHPUnit_Framework_TestCase, metaprogramming terjadi, yang mem-parsing phpDoc dan membentuk ejekan atas dasar, yang tersedia sebagai $ this-> propertyName di kelas uji. Perhatian, semua kode yang diberikan dalam artikel ini adalah pseudo-code kecil, versi lengkapnya ada di repositori:


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