How to run Codeception functional tests against a custom frameworkless web application? (that implements PSR-15 RequestHandlerInterface)

大兔子大兔子 提交于 2021-01-29 09:07:40

问题


Assume the following simple web app:

<?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;
    }
}

One can easily run it by placing the following code in their web front-controller (e.g. 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();

Now, I'd like to run Codeception feature tests against it, written in Gherkin. Consider the following simple test:

# 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'

To run acceptance tests against it, I have to provide my steps implementation. I'll reuse standard steps for that, provided by the 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

But now, I want to use same tests code to run these same tests as functional tests using Codeception. For this, I have to enable the module that implements these same steps in functional way. Which one do I use? Codeception provides several, but they're for 3rd party frameworks, e.g. Laravel, Yii2, Symphony etc. What do I do for such a simple app that doesn't use any 3rd party framework?

Here's what I've managed to do. I've created my own \Codeception\Lib\InnerBrowser implementation that inherits from \Codeception\Module\PhpBrowser provided by Codeception, in which I substitute the web client that Codeception uses (it uses Guzzle) with my own implementation (which also inherits from the Guzzle client) that doesn't perform any web requests but requests my app instead:

# 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;
}

In order for this to work, I have to make my app return Guzzle Responses (which implement PSR's ResponseInterface as well) - because PhpBrowser expects its web client to return them - which is why I had to make the ResponseFactory a constructor parameter to be able to substitute it in tests.

<?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);
    }
}

In such a configuration, everything seems to work fine.

But there's a problem. Notice the App::handle() signature:

    public function handle(RequestInterface $request): ResponseInterface

- it differs from the one that it implements, which is declared in RequestHandlerInterface:

    public function handle(ServerRequestInterface $request): ResponseInterface;

Technically, it's completely legal because it doesn't break the parameter contravariance required by the Liskov Substitution Principle. The problem that I've faced is that PhpBrowser assumes that it sends a (client-side) RequestInterfaces (over the network) but my app requires a (server-side) ServerRequestInterface instead, to be able to access parameters that are set on the server side such as ServerRequestInterface::getParsedBody(), session etc.

How do I workaround this? Framework modules provided by Codeception already do this somehow... And BTW, haven't Codeception (or someone else) provided yet an easy way to run functional tests against custom code?

Here's composer.json BTW:

{
  "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"
  }
}

来源:https://stackoverflow.com/questions/65393662/how-to-run-codeception-functional-tests-against-a-custom-frameworkless-web-appli

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!