hyperf / hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
https://www.hyperf.io
MIT License
6.21k stars 1.21k forks source link

[QUESTION] Code coverage with proxy class #6691

Open rodrifarias opened 6 months ago

rodrifarias commented 6 months ago

Hi! Hyperf creates proxy classes when starting, for correct operation. However, when running tests with code coverage, some classes are not used during testing. For example:

<?php

namespace App\Controller;

use App\Service\ServiceInterface;
use Hyperf\Di\Annotation\Inject;

#[Controller('/')]
class AppController extends AbstractController
{
    #[Inject]
    private ServiceInterface $service;

    #[GetMapping('')]
    public function serviceExecute(): string
    {
        return $this->service->execute();
    }
}

Proxy class

<?php

namespace App\Controller;

use App\Service\ServiceInterface;
use Hyperf\Di\Annotation\Inject;

#[Controller('/')]
class AppController extends AbstractController
{
    use \Hyperf\Di\Aop\ProxyTrait;
    use \Hyperf\Di\Aop\PropertyHandlerTrait;
    function __construct()
    {
        if (method_exists(parent::class, '__construct')) {
            parent::__construct(...func_get_args());
        }
        $this->__handlePropertyHandler(__CLASS__);
    }

    #[Inject]
    private ServiceInterface $service;

    #[GetMapping('')]
    public function serviceExecute(): string
    {
        return $this->service->execute();
    }
}

When running the tests the proxy class is used instead of the original class, because the ClassLoader.php map classes to proxies. Which results in invalid code coverage.

Versions: Hyperf 3.1 PHP 8.3 PHPUnit 10.5 Xdebug 3.3

Command:

php -dxdebug.mode=coverage ./vendor/bin/co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always

PHPUnit XML

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="./test/bootstrap.php" colors="true" testdox="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
    <testsuites>
        <testsuite name="Tests">
            <directory suffix=".php">./test/Unit</directory>
        </testsuite>
    </testsuites>
    <coverage includeUncoveredFiles="true" ignoreDeprecatedCodeUnits="true" disableCodeCoverageIgnore="true">
        <report>
            <html outputDirectory="coverage" lowUpperBound="50" highLowerBound="90"/>
            <clover outputFile="./coverage/coverage.xml"/>
        </report>
    </coverage>
    <logging>
        <junit outputFile=".junit/TEST-phpunit-junit.xml"/>
    </logging>
    <source>
        <include>
            <directory suffix=".php">app</directory>
        </include>
    </source>
</phpunit>

I found a similar link on stackoverflow https://stackoverflow.com/questions/78079124/how-to-do-unit-coverage-reporting-in-php-using-proxy-classes

I don't know how to solve this problem.

zds-s commented 5 months ago

Up to now, I haven't found a solution, which might be because I have a misunderstanding of code coverage or PHPUnit. If you have found a solution to this issue, I would greatly appreciate it if you could share it. Thank.

GahFlorencio commented 5 months ago

Hello i solved this creating a ClassLoaderTest extending ClassLoader and overriding the init. _{{TESTFOLDER}}/bootstrap.php

<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
use Hyperf\Contract\ApplicationInterface;
use Hyperf\Engine\DefaultOption;
use HyperfTest\ClassLoaderCustom;
use Swoole\Runtime;

/*
 * This file is part of Hyperf.
 *
 * @see     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
ini_set('display_errors', 'on');
ini_set('display_startup_errors', 'on');

error_reporting(E_ALL);
date_default_timezone_set('America/Sao_Paulo');

Runtime::enableCoroutine(true);

! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));

require BASE_PATH . '/vendor/autoload.php';

! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', DefaultOption::hookFlags());

require BASE_PATH.'/test/ClassLoaderCustom.php';

$classLoader = new ClassLoaderCustom();

$classLoader::init();

$container = require BASE_PATH . '/config/container.php';

$container->get(ApplicationInterface::class);

_{{$TESTFOLDER}} /ClassLoaderCustom.php


namespace HyperfTest;

use Hyperf\Di\ClassLoader;
use Hyperf\Di\ScanHandler\ScanHandlerInterface;

class ClassLoaderCustom extends ClassLoader
{
    public static function init(?string $proxyFileDirPath = null, ?string $configDir = null, ?ScanHandlerInterface $handler = null):void
    {
        if (file_exists(BASE_PATH . '/.env')) {
            static::loadDotenv();
        }
    }
}
rodrifarias commented 5 months ago

Hi, @GahFlorencio. I also did this, but a fundamental part of the framework stops working, which is AOP. For example cache annotations. ClassLoader creates proxy classes for correct operation when there are dependencies and behaviors before or after a certain action. I also extended the ClassLoader class, I even made it load the variables from a .env.test

GahFlorencio commented 5 months ago

I understand, as there was no error here, I believed that it would not be a problem for testing, but it is possible that I will face new errors related to AOP

zds-s commented 5 months ago

Perhaps it might only be possible to obtain a code coverage report for all classes, rather than a code coverage report for the entire project. Currently, I can only try to avoid using any methods that result in the creation of proxy classes. To my knowledge, there are many annotations in the Java Spring Boot framework that lead to the generation of proxy classes. Maybe someone could understand this issue by looking at the unit test reports from the Java side. I feel that it might not even be a problem at all; it's just that we might have gone down the wrong path.