如何针对自定义无框架 Web 应用程序运行 Codeception 功能测试? 实现 PSR-15 RequestHandlerInterface

问题描述

假设有以下简单的网络应用:

<?PHP
// src/App/App.PHP

namespace Practice\Sources\App;

use Closure;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;

class App implements RequestHandlerInterface
{
    private Closure $requestProvider;
    private ResponseFactoryInterface $responseFactory;
    private SapiEmitter $responseEmitter;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->requestProvider = fn() => ServerRequestFactory::fromGlobals();
        $this->responseFactory = $responseFactory;
        $this->responseEmitter = new SapiEmitter();
    }

    public function run(): void
    {
        $request = ($this->requestProvider)();
        $response = $this->handle($request);
        $this->responseEmitter->emit($response);
    }

    public function handle(RequestInterface $request): ResponseInterface
    {
        $response = $this->responseFactory->createResponse();
        $response->getBody()->write('hello world');
        return $response;
    }
}

可以通过将以下代码放在他们的 Web 前端控制器(例如 public_html/index.PHP)中来轻松运行它:

<?PHP
// web-root/front-controller.PHP

use Laminas\Diactoros\ResponseFactory;
use Practice\Sources\App\App;

require_once __DIR__.'/../vendor/autoload.PHP';

$responseFactory = new ResponseFactory();
$app = new App($responseFactory);
$app->run();

现在,我想对它运行 Codeception 功能测试,用 Gherkin 编写。考虑以下简单测试:

# tests/Feature/run.feature
# (also symlink'ed from tests/Codeception/tests/acceptance/ )

Feature: run app

  Scenario: I run the app
    When I am on page '/'
    Then I see 'hello world'

要针对它运行验收测试,我必须提供我的步骤实现。为此,我将重用 Codeception 提供的标准步骤:

<?PHP
// tests/Codeception/tests/_support/FeatureTester.PHP

namespace Practice\Tests\Codeception;

use Codeception\Actor;

abstract class FeatureTester extends Actor
{
//    use _generated\AcceptanceTesteractions;
//    use _generated\FunctionalTesteractions;

    /**
     * @When /^I am on page \'([^\']*)\'$/
     */
    public function iAmOnPage($page)
    {
        $this->amOnPage($page);
    }

    /**
     * @Then /^I see \'([^\']*)\'$/
     */
    public function iSee($what)
    {
        $this->see($what);
    }
}
<?PHP
// tests/Codeception/tests/_support/AcceptanceTester.PHP

namespace Practice\Tests\Codeception;

class AcceptanceTester extends FeatureTester
{
    use _generated\AcceptanceTesteractions;
}
# tests/Codeception/codeception.yml

namespace: Practice\Tests\Codeception
paths:
    tests: tests
    output: tests/_output
    data: tests/_data
    support: tests/_support
    envs: tests/_envs
actor_suffix: Tester
extensions:
    enabled:
        - Codeception\Extension\RunFailed
# tests/Codeception/tests/acceptance.suite.yml

actor: AcceptanceTester
modules:
    enabled:
        - PHPbrowser:
            url: http://practice.local
gherkin:
    contexts:
        default:
            - \Practice\Tests\Codeception\AcceptanceTester

但是现在,我想使用相同的测试代码来运行与使用 Codeception 的功能测试相同的测试。为此,我必须enable the module功能方式实现这些相同的步骤。我使用哪一种? Codeception 提供了几个,但它们是用于 3rd 方框架的,例如Laravel、Yii2、Symphony 等。对于这样一个不使用任何第三方框架的简单应用,我该怎么做?

这是我设法做到的。我创建了自己的 \Codeception\Lib\Innerbrowser 实现,它继承自 Codeception 提供的 \Codeception\Module\PHPbrowser,其中我用我自己的实现(也继承自 Guzzle)替换了 Codeception 使用的 Web 客户端(它使用 Guzzle)客户端),它不执行任何网络请求,而是请求我的应用程序:

# tests/Codeception/tests/functional.suite.yml

actor: FunctionalTester
modules:
    enabled:
        - \Practice\Tests\Codeception\Helper\CustomInnerbrowser:
            url: http://practice.local
gherkin:
    contexts:
        default:
            - \Practice\Tests\Codeception\FunctionalTester
<?PHP
// tests/Codeception/tests/_support/FunctionalTester.PHP

namespace Practice\Tests\Codeception;

class FunctionalTester extends FeatureTester
{
    use _generated\FunctionalTesteractions;
}

为了让它起作用,我必须让我的应用返回 Guzzle Responses(它也实现了 PSR 的 ResponseInterface)——因为 PHPbrowser 希望它的网络客户端返回它们 - 这就是为什么我必须将 ResponseFactory 设为构造函数参数才能在测试中替换它。

<?PHP
// tests/Codeception/tests/_support/Helper/CustomInnerbrowser.PHP

namespace Practice\Tests\Codeception\Helper;

use Codeception\Module\PHPbrowser;
use Http\Factory\Guzzle\ResponseFactory;
use Practice\Sources\App\App;

class CustomInnerbrowser extends PHPbrowser
{
    private App $app;

    public function __construct(...$args)
    {
        parent::__construct(...$args);
        $responseFactory = new ResponseFactory();
        $this->app = new App($responseFactory);
    }

    public function _prepareSession(): void
    {
        parent::_prepareSession();
        $this->guzzle = new CustomInnerbrowserClient($this->guzzle->getConfig(),$this->app);
        $this->client->setClient($this->guzzle);
    }
}
<?PHP
// tests/Codeception/tests/_support/Helper/CustomInnerbrowserClient.PHP

namespace Practice\Tests\Codeception\Helper;

use GuzzleHttp\Client as GuzzleClient;
use Practice\Sources\App\App;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class CustomInnerbrowserClient extends GuzzleClient
{
    private App $app;

    public function __construct(array $config,App $app)
    {
        parent::__construct($config);
        $this->app = $app;
    }

    public function send(RequestInterface $request,array $options = []): ResponseInterface
    {
        return $this->app->handle($request);
    }
}

在这样的配置中,一切似乎都正常。

但是一个问题。注意 App::handle() 签名:

    public function handle(RequestInterface $request): ResponseInterface

- 它与它实现的不同,它在 RequestHandlerInterface 中声明:

    public function handle(ServerRequestInterface $request): ResponseInterface;

从技术上讲,它是完全合法的,因为它没有违反 Liskov 替换原则要求的 parameter contravariance。 我面临的问题是 PHPbrowser 假定它发送一个(客户端)RequestInterfaces(通过网络)但 my 应用程序需要一个(服务器-端) ServerRequestInterface 代替,以便能够访问在服务器端设置的参数,例如 ServerRequestInterface::getParsedBody()、会话等。

我该如何解决这个问题? Codeception 提供的框架模块已经以某种方式做到了这一点......顺便说一句,Codeception(或其他人)提供了一种针对自定义代码运行功能测试的简单方法吗?

顺便说一句:composer.json

{
  "require": {
    "PHP": "~7.4","laminas/laminas-diactoros": "^2.5","laminas/laminas-httphandlerrunner": "^1.3"
  },"require-dev": {
    "codeception/codeception": "^4.1","codeception/module-PHPbrowser": "^1.0.0","http-interop/http-factory-guzzle": "^1.0"
  },"autoload": {
    "psr-4": {
      "Practice\\Sources\\": "src"
    }
  },"autoload-dev": {
    "psr-4": {
      "Practice\\Tests\\Unit\\": "tests/Unit/","Practice\\Tests\\Support\\": "tests/Support/","Practice\\Tests\\Codeception\\": "tests/Codeception/tests/_support/","Practice\\Tests\\Codeception\\_generated\\": "tests/Codeception/tests/_support/_generated/","Practice\\Tests\\Codeception\\Helper\\": "tests/Codeception/tests/_support/Helper/"
    }
  },"scripts": {
    "test-feature": "codecept run --config tests/Codeception/codeception.yml"
  }
}

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...