refactor #42 Acceptance tests for core functionalities (bartoszpietrzak1994, pamil)

This PR was merged into the 2.0-dev branch.

Discussion
----------



Commits
-------

742d3358b5 Injected parameter test POC
7250dfca76 Get bare Behat scenario running
11f5a326b3 Test injecting parameters
d4efe9a6df Simplify services definition in Behat
9cede4d612 Test injecting services
d5fcb25ef2 Test autowiring contexts
b4bcbb1233 Test autowired & autoconfigured contexts
c8b0cbb205 Test isolating contexts
6061eaaf62 Test Mink integration
05f4fe789f Apply coding standard fixes
4405596ca1 Make PHPStan passing
85796e2fef Fix build on Symfony 3.4
This commit is contained in:
Kamil Kokot
2019-01-10 15:00:45 +01:00
committed by GitHub
12 changed files with 592 additions and 226 deletions

View File

@@ -0,0 +1,133 @@
Feature: Autowiring contexts
Background:
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
suites:
default:
contexts:
- App\Tests\SomeContext
"""
Scenario: Autowiring a context with a service
Given a feature file containing:
"""
Feature:
Scenario:
Then the container should be passed
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
use Psr\Container\ContainerInterface;
final class SomeContext implements Context {
private $container;
public function __construct(?ContainerInterface $container = null) { $this->container = $container; }
/** @Then the container should be passed */
public function containerShouldBePassed(): void
{
assert(is_object($this->container));
assert($this->container instanceof ContainerInterface);
}
}
"""
And a YAML services file containing:
"""
services:
_defaults:
autowire: true
App\Tests\SomeContext:
public: true
"""
When I run Behat
Then it should pass
Scenario: Autowiring a context with a binding
Given a feature file containing:
"""
Feature:
Scenario:
Then the passed argument should be "KrzysztofKrawczyk"
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
final class SomeContext implements Context {
private $argument;
public function __construct(?string $argument = null) { $this->argument = $argument; }
/** @Then the passed argument should be :expected */
public function passedArgumentShouldBe(string $expected): void { assert($this->argument === $expected); }
}
"""
And a YAML services file containing:
"""
services:
_defaults:
autowire: true
bind:
$argument: KrzysztofKrawczyk
App\Tests\SomeContext:
public: true
"""
When I run Behat
Then it should pass
Scenario: Autowiring and autoconfiguring context based on prototype
Given a feature file containing:
"""
Feature:
Scenario:
Then the container should be passed
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
use Psr\Container\ContainerInterface;
final class SomeContext implements Context {
private $container;
public function __construct(?ContainerInterface $container = null) { $this->container = $container; }
/** @Then the container should be passed */
public function containerShouldBePassed(): void
{
assert(is_object($this->container));
assert($this->container instanceof ContainerInterface);
}
}
"""
And a YAML services file containing:
"""
services:
_defaults:
autowire: true
autoconfigure: true
App\Tests\:
resource: '../tests/*'
"""
When I run Behat
Then it should pass

View File

@@ -0,0 +1,61 @@
Feature: Injecting parameters into context
Background:
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
suites:
default:
contexts:
- App\Tests\SomeContext
"""
And a feature file containing:
"""
Feature:
Scenario:
Then the passed parameter should be "test"
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
final class SomeContext implements Context {
private $parameter;
public function __construct(?string $parameter = null) { $this->parameter = $parameter; }
/** @Then the passed parameter should be :expected */
public function parameterShouldBe(string $expected): void { assert($this->parameter === $expected); }
}
"""
Scenario: Injecting a parameter into a context explicitly set as public
Given a YAML services file containing:
"""
services:
App\Tests\SomeContext:
public: true
arguments:
- "%kernel.environment%"
"""
When I run Behat
Then it should pass
Scenario: Injecting a parameter into an autoconfigured context
Given a YAML services file containing:
"""
services:
_defaults:
autoconfigure: true
App\Tests\SomeContext:
arguments:
- "%kernel.environment%"
"""
When I run Behat
Then it should pass

View File

@@ -0,0 +1,65 @@
Feature: Injecting services into context
Background:
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
suites:
default:
contexts:
- App\Tests\SomeContext
"""
And a feature file containing:
"""
Feature:
Scenario:
Then the passed service should be an instance of "\Psr\Container\ContainerInterface"
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
final class SomeContext implements Context {
private $service;
public function __construct($service = null) { $this->service = $service; }
/** @Then the passed service should be an instance of :expected */
public function serviceShouldBe(string $expected): void
{
assert(is_object($this->service));
assert($this->service instanceof $expected);
}
}
"""
Scenario: Injecting a service into a context explicitly set as public
Given a YAML services file containing:
"""
services:
App\Tests\SomeContext:
public: true
arguments:
- "@service_container"
"""
When I run Behat
Then it should pass
Scenario: Injecting a service into an autoconfigured context
Given a YAML services file containing:
"""
services:
_defaults:
autoconfigure: true
App\Tests\SomeContext:
arguments:
- "@service_container"
"""
When I run Behat
Then it should pass

View File

@@ -0,0 +1,54 @@
Feature: Isolating contexts
Scenario: Keeping contexts isolated between scenarios
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
suites:
default:
contexts:
- App\Tests\SomeContext
"""
And a feature file containing:
"""
Feature:
Scenario: First scenario
Then the property should be "shit happens"
Scenario: Second scenario
When I change the property to "shit does not happen"
Then the property should be "shit does not happen"
Scenario: Third scenario
Then the property should be "shit happens"
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
use Psr\Container\ContainerInterface;
final class SomeContext implements Context {
private $property = 'shit happens';
public function __construct(?ContainerInterface $container = null) { $this->container = $container; }
/** @When I change the property to :value */
public function changeProperty(string $value): void { $this->property = $value; }
/** @Then the property should be :expected*/
public function propertyShouldBe(string $expected): void { assert($this->property === $expected); }
}
"""
And a YAML services file containing:
"""
services:
App\Tests\SomeContext:
public: true
"""
When I run Behat
Then it should pass

View File

@@ -0,0 +1,107 @@
Feature: Mink integration
Background:
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
extensions:
Behat\MinkExtension:
base_url: "http://localhost:8080/"
default_session: symfony
sessions:
symfony:
symfony: ~
suites:
default:
contexts:
- App\Tests\SomeContext
"""
And a feature file containing:
"""
Feature:
Scenario:
When I visit the page "/hello-world"
Then I should see "Hello world!" on the page
And the base url from Mink parameters should be "http://localhost:8080/"
# Doubling the scenario to account for some weird error connected to Mink's session
Scenario:
When I visit the page "/hello-world"
Then I should see "Hello world!" on the page
And the base url from Mink parameters should be "http://localhost:8080/"
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
use Behat\Mink\Session;
use Psr\Container\ContainerInterface;
final class SomeContext implements Context {
private $session;
private $parameters;
public function __construct(Session $session, $minkParameters)
{
if (!is_array($minkParameters) && !$minkParameters instanceof \ArrayAccess) {
throw new \InvalidArgumentException(sprintf(
'"$parameters" passed to "%s" has to be an array or implement "%s".',
self::class,
\ArrayAccess::class
));
}
$this->session = $session;
$this->parameters = $minkParameters;
}
/** @When I visit the page :page */
public function visitPage(string $page): void
{
$this->session->visit($page);
}
/** @Then I should see :content on the page */
public function shouldSeeContentOnPage(string $content): void
{
assert(false !== strpos($this->session->getPage()->getContent(), $content));
}
/** @Then the base url from Mink parameters should be :expected */
public function baseUrlShouldBe(string $expected): void
{
assert(isset($this->parameters['base_url']));
assert($this->parameters['base_url'] === $expected);
}
}
"""
Scenario: Injecting Mink session and Mink parameters
Given a YAML services file containing:
"""
services:
App\Tests\SomeContext:
public: true
arguments:
- '@behat.mink.default_session'
- '@behat.mink.parameters'
"""
When I run Behat
Then it should pass
Scenario: Autowiring and autoconfiguring Mink session and Mink parameters
Given a YAML services file containing:
"""
services:
_defaults:
autowire: true
autoconfigure: true
App\Tests\SomeContext: ~
"""
When I run Behat
Then it should pass

View File

@@ -1,9 +1,33 @@
Feature: Running bare Behat scenarios
Scenario: Running Behat with SymfonyExtension
Given a Behat configuration with the minimal working configuration for SymfonyExtension
And a Behat configuration with the minimal working configuration for MinkExtension
And an application kernel with the minimal working configuration for SymfonyExtension
And a feature file with passing scenario
Given a working Symfony application with SymfonyExtension configured
And a Behat configuration containing:
"""
default:
suites:
default:
contexts:
- App\Tests\SomeContext
"""
And a feature file containing:
"""
Feature:
Scenario:
Then it should pass
"""
And a context file "tests/SomeContext.php" containing:
"""
<?php
namespace App\Tests;
use Behat\Behat\Context\Context;
final class SomeContext implements Context {
/** @Then it should pass */
public function itShouldPass(): void {}
}
"""
When I run Behat
Then it should pass

View File

@@ -2,4 +2,7 @@ parameters:
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '/Cannot access offset 0 on callable\./'
- '/Cannot access offset 1 on callable\./'
- '/Method FriendsOfBehat\\SymfonyExtension\\Context\\Environment\\InitialisedContextServiceEnvironment::bindCallee\(\) should return callable/'
- '/Cannot call method [a-zA-Z0-9]+\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface|null\./'

View File

@@ -24,6 +24,9 @@ final class FriendsOfBehatSymfonyExtensionExtension extends Extension implements
$container
->registerForAutoconfiguration(Context::class)
->addTag('fob.context')
->setBindings([
'$minkParameters' => new Reference('behat.mink.parameters'),
])
;
}

View File

@@ -60,11 +60,12 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler
}
/**
* @param UninitialisedContextServiceEnvironment $uninitializedEnvironment
*
* @throws EnvironmentIsolationException
*/
public function isolateEnvironment(Environment $uninitializedEnvironment, $testSubject = null): Environment
{
/** @var UninitialisedContextServiceEnvironment $uninitializedEnvironment */
$this->assertEnvironmentCanBeIsolated($uninitializedEnvironment, $testSubject);
$environment = new InitialisedContextServiceEnvironment($uninitializedEnvironment->getSuite());
@@ -136,22 +137,31 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler
return $class;
}
throw new \Exception('wtf?');
throw new \DomainException(sprintf('There is no service or class "%s".', $contextId));
}
private function getContext(string $contextId): Context
{
if ($this->getContainer()->has($contextId)) {
return $this->getContainer()->get($contextId);
}
$class = '\\' . ltrim($contextId, '\\');
if (class_exists($class)) {
return new $class();
if ($this->getContainer()->has($contextId)) {
$context = $this->getContainer()->get($contextId);
} elseif (class_exists($class)) {
$context = new $class();
} else {
throw new \DomainException(sprintf('There is no service or class "%s".', $contextId));
}
throw new \Exception('wtf?');
if (!$context instanceof Context) {
throw new \DomainException(sprintf(
'Context "%s" referenced as "%s" needs to implement "%s".',
get_class($context),
$contextId,
Context::class
));
}
return $context;
}
private function getContainer(): ContainerInterface

View File

@@ -22,6 +22,16 @@ final class SymfonyDriver extends BrowserKitDriver
));
}
parent::__construct($kernel->getContainer()->get('test.client'), $baseUrl);
$testClient = $kernel->getContainer()->get('test.client');
if (!$testClient instanceof Client) {
throw new \RuntimeException(sprintf(
'Service "test.client" should be an instance of "%s", "%s" given.',
Client::class,
get_class($testClient)
));
}
parent::__construct($testClient, $baseUrl);
}
}

View File

@@ -36,6 +36,9 @@ final class SymfonyExtension implements Extension
*/
private const DRIVER_KERNEL_ID = 'fob_symfony.driver_kernel';
/** @var bool */
private $minkExtensionFound = false;
public function getConfigKey(): string
{
return 'fob_symfony';
@@ -43,7 +46,14 @@ final class SymfonyExtension implements Extension
public function initialize(ExtensionManager $extensionManager): void
{
$this->registerSymfonyDriverFactory($extensionManager);
/** @var MinkExtension|null $minkExtension */
$minkExtension = $extensionManager->getExtension('mink');
if (null === $minkExtension) {
return;
}
$minkExtension->registerDriverFactory(new SymfonyDriverFactory('symfony', new Reference(self::DRIVER_KERNEL_ID)));
$this->minkExtensionFound = true;
}
public function configure(ArrayNodeDefinition $builder): void
@@ -75,8 +85,10 @@ final class SymfonyExtension implements Extension
$this->loadEnvironmentHandler($container);
$this->loadMinkDefaultSession($container);
$this->loadMinkParameters($container);
if ($this->minkExtensionFound) {
$this->loadMinkDefaultSession($container);
$this->loadMinkParameters($container);
}
}
public function process(ContainerBuilder $container): void
@@ -166,15 +178,4 @@ final class SymfonyExtension implements Extension
$container->setDefinition('fob_symfony.mink.parameters', $minkParametersDefinition);
}
private function registerSymfonyDriverFactory(ExtensionManager $extensionManager): void
{
/** @var MinkExtension|null $minkExtension */
$minkExtension = $extensionManager->getExtension('mink');
if (null === $minkExtension) {
return;
}
$minkExtension->registerDriverFactory(new SymfonyDriverFactory('symfony', new Reference(self::DRIVER_KERNEL_ID)));
}
}

View File

@@ -12,24 +12,16 @@ use Symfony\Component\Yaml\Yaml;
final class TestContext implements Context
{
/**
* @var string
*/
/** @var string */
private static $workingDir;
/**
* @var Filesystem
*/
/** @var Filesystem */
private static $filesystem;
/**
* @var string
*/
/** @var string */
private static $phpBin;
/**
* @var Process
*/
/** @var Process */
private $process;
/**
@@ -59,13 +51,98 @@ final class TestContext implements Context
self::$filesystem->remove(self::$workingDir);
}
/**
* @Given a working Symfony application with SymfonyExtension configured
*/
public function workingSymfonyApplicationWithExtension(): void
{
$this->thereIsConfiguration(<<<'CON'
default:
extensions:
FriendsOfBehat\SymfonyExtension:
kernel:
class: App\Kernel
CON
);
$this->thereIsFile('vendor/autoload.php', sprintf(<<<'CON'
<?php
declare(strict_types=1);
$loader = require '%s';
$loader->addPsr4('App\\', __DIR__ . '/../src/');
$loader->addPsr4('App\\Tests\\', __DIR__ . '/../tests/');
return $loader;
CON
, __DIR__ . '/../../../vendor/autoload.php'));
$this->thereIsFile('src/Kernel.php', <<<'CON'
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Kernel as HttpKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends HttpKernel
{
use MicroKernelTrait;
public function helloWorld(): Response
{
return new Response('Hello world!');
}
public function registerBundles(): iterable
{
return [
new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new \FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle(),
];
}
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$container->loadFromExtension('framework', [
'test' => $this->getEnvironment() === 'test',
'secret' => 'Pigeon',
]);
$loader->load(__DIR__ . '/../config/services.yaml');
}
protected function configureRoutes(RouteCollectionBuilder $routes)
{
$routes->add('/hello-world', 'kernel:helloWorld');
}
}
CON
);
$this->thereIsFile('config/services.yaml', '');
}
/**
* @Given /^a YAML services file containing:$/
*/
public function yamlServicesFile($content): void
{
$this->thereIsFile('config/services.yaml', (string) $content);
}
/**
* @Given /^a Behat configuration containing(?: "([^"]+)"|:)$/
*/
public function thereIsConfiguration($content): void
{
$mainConfigFile = sprintf('%s/behat.yml', self::$workingDir);
$newConfigFile = sprintf('%s/behat-%s.yml', self::$workingDir, md5($content));
$newConfigFile = sprintf('%s/behat-%s.yml', self::$workingDir, md5((string) $content));
self::$filesystem->dumpFile($newConfigFile, (string) $content);
@@ -79,40 +156,6 @@ final class TestContext implements Context
self::$filesystem->dumpFile($mainConfigFile, Yaml::dump($mainBehatConfiguration));
}
/**
* @Given /^a Behat configuration with the minimal working configuration for SymfonyExtension$/
*/
public function thereIsConfigurationWithMinimalWorkingConfigurationForSymfonyExtension(): void
{
$this->thereIsConfiguration(<<<'CON'
default:
extensions:
FriendsOfBehat\SymfonyExtension:
kernel:
path: app/AppKernel.php
class: AppKernel
CON
);
}
/**
* @Given /^a Behat configuration with the minimal working configuration for MinkExtension$/
*/
public function thereIsConfigurationWithMinimalWorkingConfigurationForMinkExtension(): void
{
$this->thereIsConfiguration(<<<'CON'
default:
extensions:
Behat\MinkExtension:
base_url: "http://localhost:8080/"
default_session: symfony
sessions:
symfony:
symfony: ~
CON
);
}
/**
* @Given /^a (?:.+ |)file "([^"]+)" containing(?: "([^"]+)"|:)$/
*/
@@ -121,42 +164,6 @@ CON
self::$filesystem->dumpFile(self::$workingDir . '/' . $file, (string) $content);
}
/**
* @Given /^an application kernel with the minimal working configuration for SymfonyExtension$/
*/
public function thereIsKernelWithMinimalWorkingConfiguration(): void
{
$this->thereIsFile('app/AppKernel.php', <<<'CON'
<?php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
public function registerBundles()
{
return [
new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new \FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle(),
];
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container): void {
$container->loadFromExtension('framework', [
'test' => $this->getEnvironment() === 'test',
'secret' => 'Pigeon',
]);
});
}
}
CON
);
}
/**
* @Given /^a feature file containing(?: "([^"]+)"|:)$/
*/
@@ -165,110 +172,6 @@ CON
$this->thereIsFile(sprintf('features/%s.feature', md5(uniqid('', true))), $content);
}
/**
* @Given /^a feature file with passing scenario$/
*/
public function thereIsFeatureFileWithPassingScenario(): void
{
$this->thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON'
<?php
declare(strict_types=1);
class FeatureContext implements \Behat\Behat\Context\Context
{
/** @Then it passes */
public function itPasses() {}
}
CON
);
$this->thereIsFeatureFile(<<<FEA
Feature: Passing feature
Scenario: Passing scenario
Then it passes
FEA
);
}
/**
* @Given /^a feature file with failing scenario$/
*/
public function thereIsFeatureFileWithFailingScenario(): void
{
$this->thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON'
<?php
declare(strict_types=1);
class FeatureContext implements \Behat\Behat\Context\Context
{
/** @Then it fails */
public function itFails() { throw new \RuntimeException(); }
}
CON
);
$this->thereIsFeatureFile(<<<FEA
Feature: Failing feature
Scenario: Failing scenario
Then it fails
FEA
);
}
/**
* @Given /^a feature file with scenario with missing step$/
*/
public function thereIsFeatureFileWithScenarioWithMissingStep(): void
{
$this->thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON'
<?php
declare(strict_types=1);
class FeatureContext implements \Behat\Behat\Context\Context {}
CON
);
$this->thereIsFeatureFile(<<<FEA
Feature: Feature with missing step
Scenario: Scenario with missing step
Then it does not have this step
FEA
);
}
/**
* @Given /^a feature file with scenario with pending step$/
*/
public function thereIsFeatureFileWithScenarioWithPendingStep(): void
{
$this->thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON'
<?php
declare(strict_types=1);
class FeatureContext implements \Behat\Behat\Context\Context
{
/** @Then it has this step as pending */
public function itFails() { throw new \Behat\Behat\Tester\Exception\PendingException(); }
}
CON
);
$this->thereIsFeatureFile(<<<FEA
Feature: Feature with pending step
Scenario: Scenario with pending step
Then it has this step as pending
FEA
);
}
/**
* @When /^I run Behat$/
*/
@@ -290,7 +193,7 @@ FEA
}
throw new \DomainException(
'Behat was expecting to pass, but failed with the following output:' . PHP_EOL . PHP_EOL . $this->getProcessOutput()
'Behat was expecting to pass, but failed with the following output:' . \PHP_EOL . \PHP_EOL . $this->getProcessOutput()
);
}
@@ -313,7 +216,7 @@ FEA
}
throw new \DomainException(
'Behat was expecting to fail, but passed with the following output:' . PHP_EOL . PHP_EOL . $this->getProcessOutput()
'Behat was expecting to fail, but passed with the following output:' . \PHP_EOL . \PHP_EOL . $this->getProcessOutput()
);
}
@@ -349,16 +252,13 @@ FEA
if (0 === $result) {
throw new \DomainException(sprintf(
'Pattern "%s" does not match the following output:' . PHP_EOL . PHP_EOL . '%s',
'Pattern "%s" does not match the following output:' . \PHP_EOL . \PHP_EOL . '%s',
$pattern,
$output
));
}
}
/**
* @return string
*/
private function getProcessOutput(): string
{
$this->assertProcessIsAvailable();
@@ -366,9 +266,6 @@ FEA
return $this->process->getErrorOutput() . $this->process->getOutput();
}
/**
* @return int
*/
private function getProcessExitCode(): int
{
$this->assertProcessIsAvailable();
@@ -387,8 +284,6 @@ FEA
}
/**
* @return string
*
* @throws \RuntimeException
*/
private static function findPhpBinary(): string