NEW LocatorProcessor and ClassGuesser to manage modules

Important for running behat tests per-module,
while stil keeping parameters like "base_url" configurable
without modifying behat.yml files in the modules own source code.
This commit is contained in:
Ingo Schommer 2012-11-14 15:21:12 +01:00
parent 046350fcce
commit feba280470
4 changed files with 203 additions and 33 deletions

View File

@ -57,10 +57,7 @@ And get the latest Selenium2 server (requires Java):
As a convention, SilverStripe Behat tests live in a `tests/behat` subfolder
of your module. You can create it with the following command:
cd mymodule
mkdir -p tests/behat
cd tests/behat
../../../vendor/bin/behat --init
mkdir -p mymodule/tests/behat/features/bootstrap/MyModule/Test/Behaviour
### FeatureContext
@ -69,10 +66,10 @@ here as well. The only major difference is the base class from which
to extend your own `FeatureContext`: It should be `SilverStripeContext`
rather than `BehatContext`.
Example: FeatureContext.php
Example: mymodule/tests/behat/features/bootstrap/MyModule/Test/Behaviour/FeatureContext.php
<?php
namespace SilverStripe\MyModule\Test\Behaviour;
namespace MyModule\Test\Behaviour;
use SilverStripe\BehatExtension\Context\SilverStripeContext,
SilverStripe\BehatExtension\Context\BasicContext,
@ -94,7 +91,7 @@ Example: FeatureContext.php
### behat.yml
Create a `mymodule/tests/behat/behat.yml` file, and add the template below.
Create a `behat.yml` file in the project root, and add the template below.
TODO: Move to auto-loaded configuration
@ -134,23 +131,6 @@ of a failed step. It defaults to whatever is returned by PHP's `sys_get_temp_dir
Screenshot names within that directory consist of feature file filename and line
number that failed.
### Additional profiles
By default, `MinkExtension` is using `FirefoxDriver`.
Let's say you want to user `ChromeDriver` too.
You can either override the `selenium2` setting in default profile or add another
profile that can be run using `bin/behat --profile=PROFILE_NAME`, where `PROFILE_NAME`
could be `chrome`.
chrome:
extensions:
Behat\MinkExtension\Extension:
selenium2:
capabilities:
browserName: chrome
version: ANY
## Usage
### Starting the selenium server
@ -171,14 +151,11 @@ You will have Behat binary located in `bin` directory in your project root (or w
By default, Behat will use Selenium2 driver.
Selenium will also try to use chrome browser. Refer to `behat.yml` for details.
# This will run all feature tests located in `features` directory
vendor/bin/behat --config mymodule/tests/behat/behat.yml
# Run all "mymodule" tests
vendor/bin/behat @mymodule
# This will run all feature tests using chrome profile
vendor/behat --config mymodule/tests/behat/behat.yml --profile=chrome
# This will run a specific feature test
vendor/behat --config mymodule/tests/behat/behat.yml mymodule/tests/behat/features/my-steps.feature
# Run a specific feature test
vendor/behat @mymodule/my-steps.feature
### Available Step Definitions
@ -186,7 +163,7 @@ The extension comes with several `BehatContext` subclasses come with some extra
Some of them are just helpful in general website testing, other's are specific to SilverStripe.
To find out all available steps (and the files they are defined in), run the following:
vendor/bin/behat --config mymodule/tests/behat/behat.yml --definitions=i
vendor/bin/behat @mymodule --definitions=i
Note: There are more specific step definitions in the SilverStripe `framework` module
for interacting with the CMS interfaces (see `framework/tests/behat/features/bootstrap`).
@ -243,6 +220,25 @@ The module runner empties the database before each scenario tagged with
`@database-defaults` and populates it with default records (usually a set of
default pages).
## Howto
### Additional profiles
By default, `MinkExtension` is using `FirefoxDriver`.
Let's say you want to use `ChromeDriver` too.
You can either override the `selenium2` setting in default profile or add another
profile that can be run using `bin/behat --profile=PROFILE_NAME`, where `PROFILE_NAME`
could be `chrome`.
chrome:
extensions:
Behat\MinkExtension\Extension:
selenium2:
capabilities:
browserName: chrome
version: ANY
## FAQ
### Why does the module need to know about the framework path on the filesystem?

View File

@ -0,0 +1,110 @@
<?php
namespace SilverStripe\BehatExtension\Console\Processor;
use Symfony\Component\DependencyInjection\ContainerInterface,
Symfony\Component\Console\Command\Command,
Symfony\Component\Console\Input\InputArgument,
Symfony\Component\Console\Input\InputInterface,
Symfony\Component\Console\Output\OutputInterface;
use Behat\Behat\Console\Processor\LocatorProcessor as BaseProcessor;
/**
* Path locator processor.
*/
class LocatorProcessor extends BaseProcessor
{
private $container;
/**
* Constructs processor.
*
* @param ContainerInterface $container Container instance
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Configures command to be able to process it later.
*
* @param Command $command
*/
public function configure(Command $command)
{
$command->addArgument('features', InputArgument::OPTIONAL,
"Feature(s) to run. Could be:".
"\n- a dir (<comment>src/to/module/Features/</comment>), " .
"\n- a feature (<comment>src/to/module/Features/*.feature</comment>), " .
"\n- a scenario at specific line (<comment>src/to/module/Features/*.feature:10</comment>). " .
"\n- Also, you can use short module notation (<comment>@moduleName/*.feature:10</comment>)"
);
}
/**
* Processes data from container and console input.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @throws \RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output)
{
// Bootstrap SS so we can use module listing
$frameworkPath = $this->container->getParameter('behat.silverstripe_extension.framework_path');
$_GET['flush'] = 1;
require_once $frameworkPath . '/core/Core.php';
unset($_GET['flush']);
$featuresPath = $input->getArgument('features');
$pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix');
$currentModuleName = null;
$modules = \SS_ClassLoader::instance()->getManifest()->getModules();
// get module specified in behat.yml
$currentModuleName = $this->container->getParameter('behat.silverstripe_extension.module');
// get module from short notation if path starts from @
if ($featuresPath && preg_match('/^\@([^\/\\\\]+)(.*)$/', $featuresPath, $matches)) {
$currentModuleName = $matches[1];
// TODO Replace with proper module loader once AJShort's changes are merged into core
$currentModulePath = $modules[$currentModuleName];
$featuresPath = str_replace(
'@'.$currentModuleName,
$currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix,
$featuresPath
);
// get module from provided features path
} elseif (!$currentModuleName && $featuresPath) {
$path = realpath(preg_replace('/\.feature\:.*$/', '.feature', $featuresPath));
foreach ($modules as $moduleName => $modulePath) {
if (false !== strpos($path, realpath($modulePath))) {
$currentModuleName = $moduleName;
break;
}
}
// if module is configured for profile and feature provided
} elseif ($currentModuleName && $featuresPath) {
$currentModulePath = $modules[$currentModuleName];
$featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath;
}
if ($currentModuleName) {
$this->container
->get('behat.silverstripe_extension.context.class_guesser')
->setModuleNamespace(ucfirst($currentModuleName));
}
if (!$featuresPath) {
$featuresPath = $this->container->getParameter('behat.paths.features');
}
$this->container
->get('behat.console.command')
->setFeaturesPaths($featuresPath ? array($featuresPath) : array());
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\BehatExtension\Context\ClassGuesser;
use Behat\Behat\Context\ClassGuesser\ClassGuesserInterface;
/**
* Module context class guesser.
* Provides module context class if found.
*/
class ModuleContextClassGuesser implements ClassGuesserInterface
{
private $classSuffix;
private $namespace;
/**
* Initializes guesser.
*
* @param string $classSuffix
*/
public function __construct($classSuffix = 'Test\\Behaviour\\FeatureContext')
{
$this->classSuffix = $classSuffix;
}
/**
* Sets bundle namespace to use for guessing.
*
* @param string $namespace
*/
public function setModuleNamespace($namespace)
{
$this->namespace = $namespace;
}
/**
* Tries to guess context classname.
*
* @return string
*/
public function guess()
{
// Try fully qualified namespace
if (class_exists($class = $this->namespace.'\\'.$this->classSuffix)) {
return $class;
}
// Fall back to namespace with SilverStripe prefix
// TODO Remove once core has namespace capabilities for modules
if (class_exists($class = 'SilverStripe\\'.$this->namespace.'\\'.$this->classSuffix)) {
return $class;
}
}
}

View File

@ -1,11 +1,16 @@
parameters:
behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer
behat.silverstripe_extension.framework_path: ~
behat.silverstripe_extension.context.class_guesser.class: SilverStripe\BehatExtension\Context\ClassGuesser\ModuleContextClassGuesser
behat.console.processor.locator.class: SilverStripe\BehatExtension\Console\Processor\LocatorProcessor
behat.silverstripe_extension.context.class_suffix: Test\Behaviour\FeatureContext
behat.silverstripe_extension.framework_path: framework
behat.silverstripe_extension.ajax_steps: ~
behat.silverstripe_extension.ajax_timeout: ~
behat.silverstripe_extension.admin_url: ~
behat.silverstripe_extension.login_url: ~
behat.silverstripe_extension.screenshot_path: ~
behat.silverstripe_extension.module:
behat.silverstripe_extension.context.path_suffix: tests/behat/features/
services:
behat.silverstripe_extension.context.initializer:
class: %behat.silverstripe_extension.context.initializer.class%
@ -20,3 +25,9 @@ services:
- [setScreenshotPath, [%behat.silverstripe_extension.screenshot_path%]]
tags:
- { name: behat.context.initializer }
behat.silverstripe_extension.context.class_guesser:
class: %behat.silverstripe_extension.context.class_guesser.class%
arguments:
- %behat.silverstripe_extension.context.class_suffix%
tags:
- { name: behat.context.class_guesser, priority: 10 }