diff --git a/features/autowiring_contexts.feature b/features/autowiring_contexts.feature new file mode 100644 index 0000000..c6b9f40 --- /dev/null +++ b/features/autowiring_contexts.feature @@ -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: + """ + 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: + """ + 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: + """ + 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 diff --git a/features/injecting_parameters_into_context.feature b/features/injecting_parameters_into_context.feature new file mode 100644 index 0000000..3d63e88 --- /dev/null +++ b/features/injecting_parameters_into_context.feature @@ -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: + """ + 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 diff --git a/features/injecting_services_into_context.feature b/features/injecting_services_into_context.feature new file mode 100644 index 0000000..f8b60c7 --- /dev/null +++ b/features/injecting_services_into_context.feature @@ -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: + """ + 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 diff --git a/features/isolating_contexts.feature b/features/isolating_contexts.feature new file mode 100644 index 0000000..6cfe6cd --- /dev/null +++ b/features/isolating_contexts.feature @@ -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: + """ + 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 diff --git a/features/mink_integration.feature b/features/mink_integration.feature new file mode 100644 index 0000000..7dad88e --- /dev/null +++ b/features/mink_integration.feature @@ -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: + """ + 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 diff --git a/features/running_bare_behat_scenarios.feature b/features/running_bare_behat_scenarios.feature index 9febc9a..cc2768c 100644 --- a/features/running_bare_behat_scenarios.feature +++ b/features/running_bare_behat_scenarios.feature @@ -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: + """ + registerForAutoconfiguration(Context::class) ->addTag('fob.context') + ->setBindings([ + '$minkParameters' => new Reference('behat.mink.parameters'), + ]) ; } diff --git a/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php b/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php index d7e5d35..8792d85 100644 --- a/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php +++ b/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php @@ -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 diff --git a/src/Driver/SymfonyDriver.php b/src/Driver/SymfonyDriver.php index e2d1d26..dbbc18a 100644 --- a/src/Driver/SymfonyDriver.php +++ b/src/Driver/SymfonyDriver.php @@ -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); } } diff --git a/src/ServiceContainer/SymfonyExtension.php b/src/ServiceContainer/SymfonyExtension.php index 215054c..42d6d9a 100644 --- a/src/ServiceContainer/SymfonyExtension.php +++ b/src/ServiceContainer/SymfonyExtension.php @@ -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))); - } } diff --git a/tests/Behat/Context/TestContext.php b/tests/Behat/Context/TestContext.php index 8dae841..1398c1f 100644 --- a/tests/Behat/Context/TestContext.php +++ b/tests/Behat/Context/TestContext.php @@ -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' +addPsr4('App\\', __DIR__ . '/../src/'); +$loader->addPsr4('App\\Tests\\', __DIR__ . '/../tests/'); + +return $loader; +CON + , __DIR__ . '/../../../vendor/autoload.php')); + + $this->thereIsFile('src/Kernel.php', <<<'CON' +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' -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' -thereIsFeatureFile(<<thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON' -thereIsFeatureFile(<<thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON' -thereIsFeatureFile(<<thereIsFile('features/bootstrap/FeatureContext.php', <<<'CON' -thereIsFeatureFile(<<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