Initializer and custom namespace support

This commit is contained in:
Ingo Schommer 2013-10-18 17:00:07 +02:00
parent 6b1b4d3d94
commit 5ca904ae12
7 changed files with 337 additions and 25 deletions

View File

@ -131,7 +131,34 @@ Example: behat.yml
selenium2: selenium2:
browser: firefox browser: firefox
### Available Step Definitions ## Module Initialization
You're all set to start writing features now! Simply create `*.feature` files
anywhere in your codebase, and run them as shown above. We recommend the folder
structure of `tests/behat/features`, since its consistent with the common location
of SilverStripe's PHPUnit tests.
Behat tests rely on a `FeatureContext` class which contains step definitions,
and can be composed of other subcontexts, e.g. for SilverStripe-specific CMS steps
(details on [behat.org](http://docs.behat.org/quick_intro.html#the-context-class-featurecontext)).
Since step definitions are quite domain specific, its likely that you'll need your own context.
The SilverStripe Behat extension provides an initializer script which generates a template
in the recommended folder structure:
vendor/bin/behat --init @mymodule
You'll now have a class located in `mymodule/tests/behat/features/bootstrap/Context/FeatureContext.php`,
as well as a folder for your features with `mymodule/tests/behat/features`.
The class is namespaced, and defaults to the module name. You can customize this:
vendor/bin/behat --namespace='MyVendor\MyModule' --init @mymodule
In this case, you'll need to pass in the namespace when running the features as well
(at least until SilverStripe modules allow declaring a namespace).
vendor/bin/behat --namespace='MyVendor\MyModule' @mymodule
## Available Step Definitions
The extension comes with several `BehatContext` subclasses come with some extra step defintions. The extension comes with several `BehatContext` subclasses come with some extra step defintions.
Some of them are just helpful in general website testing, other's are specific to SilverStripe. Some of them are just helpful in general website testing, other's are specific to SilverStripe.

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\BehatExtension\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder,
Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Loads SilverStripe core. Required to initialize autoloading.
*/
class CoreInitializationPass implements CompilerPassInterface
{
/**
* Loads kernel file.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
// Connect to database and build manifest
$frameworkPath = $container->getParameter('behat.silverstripe_extension.framework_path');
$_GET['flush'] = 1;
require_once $frameworkPath . '/core/Core.php';
\TestRunner::use_test_manifest();
unset($_GET['flush']);
// Remove the error handler so that PHPUnit can add its own
restore_error_handler();
}
}

View File

@ -0,0 +1,247 @@
<?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,
Symfony\Component\Console\Input\InputOption;
use Behat\Behat\Console\Processor\InitProcessor as BaseProcessor;
/**
* Initializes a project for Behat usage, creating context files.
*/
class InitProcessor extends BaseProcessor
{
private $container;
/**
* @param ContainerInterface $container Container instance
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @param Command $command
*/
public function configure(Command $command)
{
parent::configure($command);
$command->addOption('--namespace', null, InputOption::VALUE_OPTIONAL,
"Optional namespace for FeatureContext, defaults to <foldername>\\Test\\Behaviour.\n"
);
}
public function process(InputInterface $input, OutputInterface $output)
{
// throw exception if no features argument provided
if (!$input->getArgument('features') && $input->getOption('init')) {
throw new \InvalidArgumentException('Provide features argument in order to init suite.');
}
// initialize bundle structure and exit
if ($input->getOption('init')) {
$this->initBundleDirectoryStructure($input, $output);
exit(0);
}
}
/**
* Inits bundle directory structure
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function initBundleDirectoryStructure(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');
if(!$featuresPath) {
throw new \InvalidArgumentException('Please specify a module name (e.g. "@mymodule")');
}
// Can't use 'behat.paths.base' since that's locked at this point to base folder (not module)
$pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix');
$currentModuleName = null;
$modules = \SS_ClassLoader::instance()->getManifest()->getModules();
$currentModuleName = $this->container->getParameter('behat.silverstripe_extension.module');
// get module from short notation if path starts from @
if (preg_match('/^\@([^\/\\\\]+)(.*)$/', $featuresPath, $matches)) {
$currentModuleName = $matches[1];
// TODO Replace with proper module loader once AJShort's changes are merged into core
if (!array_key_exists($currentModuleName, $modules)) {
throw new \InvalidArgumentException(sprintf('Module "%s" not found', $currentModuleName));
}
$currentModulePath = $modules[$currentModuleName];
}
if (!$currentModuleName) {
throw new \InvalidArgumentException('Can not find module to initialize suite.');
}
// TODO Retrieve from module definition once that's implemented
if($input->getOption('namespace')) {
$namespace = $input->getOption('namespace');
} else {
$namespace = ucfirst($currentModuleName);
}
$namespace .= '\\' . $this->container->getParameter('behat.silverstripe_extension.context.namespace_suffix');
$featuresPath = rtrim($currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix,DIRECTORY_SEPARATOR);
$basePath = $this->container->getParameter('behat.paths.base').DIRECTORY_SEPARATOR;
$bootstrapPath = $featuresPath.DIRECTORY_SEPARATOR.'bootstrap';
$contextPath = $bootstrapPath.DIRECTORY_SEPARATOR.'Context';
if (!is_dir($featuresPath)) {
mkdir($featuresPath, 0777, true);
mkdir($bootstrapPath, 0777, true);
// touch($bootstrapPath.DIRECTORY_SEPARATOR.'_manifest_exclude');
$output->writeln(
'<info>+d</info> ' .
str_replace($basePath, '', realpath($featuresPath)) .
' <comment>- place your *.feature files here</comment>'
);
}
if (!is_dir($contextPath)) {
mkdir($contextPath, 0777, true);
$className = $this->container->getParameter('behat.context.class');
file_put_contents(
$contextPath . DIRECTORY_SEPARATOR . $className . '.php',
strtr($this->getFeatureContextSkelet(), array(
'%NAMESPACE%' => $namespace
))
);
$output->writeln(
'<info>+f</info> ' .
str_replace($basePath, '', realpath($contextPath)) . DIRECTORY_SEPARATOR .
'FeatureContext.php <comment>- place your feature related code here</comment>'
);
}
}
/**
* {@inheritdoc}
*/
protected function getFeatureContextSkelet()
{
return <<<'PHP'
<?php
namespace %NAMESPACE%;
use SilverStripe\BehatExtension\Context\SilverStripeContext,
SilverStripe\BehatExtension\Context\BasicContext,
SilverStripe\BehatExtension\Context\LoginContext,
SilverStripe\BehatExtension\Context\FixtureContext,
SilverStripe\Framework\Test\Behaviour\CmsFormsContext,
SilverStripe\Framework\Test\Behaviour\CmsUiContext,
SilverStripe\Cms\Test\Behaviour;
// PHPUnit
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
/**
* Features context
*
* Context automatically loaded by Behat.
* Uses subcontexts to extend functionality.
*/
class FeatureContext extends SilverStripeContext {
/**
* @var FixtureFactory
*/
protected $fixtureFactory;
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters) {
parent::__construct($parameters);
$this->useContext('BasicContext', new BasicContext($parameters));
$this->useContext('LoginContext', new LoginContext($parameters));
$this->useContext('CmsFormsContext', new CmsFormsContext($parameters));
$this->useContext('CmsUiContext', new CmsUiContext($parameters));
$fixtureContext = new FixtureContext($parameters);
$fixtureContext->setFixtureFactory($this->getFixtureFactory());
$this->useContext('FixtureContext', $fixtureContext);
// Use blueprints to set user name from identifier
$factory = $fixtureContext->getFixtureFactory();
$blueprint = \Injector::inst()->create('FixtureBlueprint', 'Member');
$blueprint->addCallback('beforeCreate', function($identifier, &$data, &$fixtures) {
if(!isset($data['FirstName'])) $data['FirstName'] = $identifier;
});
$factory->define('Member', $blueprint);
// Auto-publish pages
foreach(\ClassInfo::subclassesFor('SiteTree') as $id => $class) {
$blueprint = \Injector::inst()->create('FixtureBlueprint', $class);
$blueprint->addCallback('afterCreate', function($obj, $identifier, &$data, &$fixtures) {
$obj->publish('Stage', 'Live');
});
$factory->define($class, $blueprint);
}
}
public function setMinkParameters(array $parameters) {
parent::setMinkParameters($parameters);
if(isset($parameters['files_path'])) {
$this->getSubcontext('FixtureContext')->setFilesPath($parameters['files_path']);
}
}
/**
* @return FixtureFactory
*/
public function getFixtureFactory() {
if(!$this->fixtureFactory) {
$this->fixtureFactory = \Injector::inst()->create('BehatFixtureFactory');
}
return $this->fixtureFactory;
}
public function setFixtureFactory(FixtureFactory $factory) {
$this->fixtureFactory = $factory;
}
//
// Place your definition and hook methods here:
//
// /**
// * @Given /^I have done something with "([^"]*)"$/
// */
// public function iHaveDoneSomethingWith($argument) {
// $container = $this->kernel->getContainer();
// $container->get('some_service')->doSomethingWith($argument);
// }
//
}
PHP;
}
}

View File

@ -53,13 +53,9 @@ class LocatorProcessor extends BaseProcessor
*/ */
public function process(InputInterface $input, OutputInterface $output) 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'); $featuresPath = $input->getArgument('features');
// Can't use 'behat.paths.base' since that's locked at this point to base folder (not module)
$pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix'); $pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix');
$currentModuleName = null; $currentModuleName = null;
@ -84,23 +80,28 @@ class LocatorProcessor extends BaseProcessor
foreach ($modules as $moduleName => $modulePath) { foreach ($modules as $moduleName => $modulePath) {
if (false !== strpos($path, realpath($modulePath))) { if (false !== strpos($path, realpath($modulePath))) {
$currentModuleName = $moduleName; $currentModuleName = $moduleName;
$currentModulePath = realpath($modulePath);
break; break;
} }
} }
$featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath;
// if module is configured for profile and feature provided // if module is configured for profile and feature provided
} elseif ($currentModuleName && $featuresPath) { } elseif ($currentModuleName && $featuresPath) {
$currentModulePath = $modules[$currentModuleName]; $currentModulePath = $modules[$currentModuleName];
$featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath; $featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath;
} }
if($input->getOption('namespace')) {
$namespace = $input->getOption('namespace');
} else {
$namespace = ucfirst($currentModuleName);
}
if ($currentModuleName) { if ($currentModuleName) {
$this->container $this->container
->get('behat.silverstripe_extension.context.class_guesser') ->get('behat.silverstripe_extension.context.class_guesser')
->setModuleNamespace(ucfirst($currentModuleName)); // TODO Improve once modules can declare their own namespaces consistently
} ->setNamespaceBase($namespace);
if (!$featuresPath) {
$featuresPath = $this->container->getParameter('behat.paths.features');
} }
$this->container $this->container

View File

@ -10,17 +10,19 @@ use Behat\Behat\Context\ClassGuesser\ClassGuesserInterface;
*/ */
class ModuleContextClassGuesser implements ClassGuesserInterface class ModuleContextClassGuesser implements ClassGuesserInterface
{ {
private $classSuffix; private $namespaceSuffix;
private $namespace; private $namespaceBase;
private $contextClass;
/** /**
* Initializes guesser. * Initializes guesser.
* *
* @param string $classSuffix * @param string $namespaceSuffix
*/ */
public function __construct($classSuffix = 'Test\\Behaviour\\FeatureContext') public function __construct($namespaceSuffix, $contextClass)
{ {
$this->classSuffix = $classSuffix; $this->namespaceSuffix = $namespaceSuffix;
$this->contextClass = $contextClass;
} }
/** /**
@ -28,9 +30,10 @@ class ModuleContextClassGuesser implements ClassGuesserInterface
* *
* @param string $namespace * @param string $namespace
*/ */
public function setModuleNamespace($namespace) public function setNamespaceBase($namespaceBase)
{ {
$this->namespace = $namespace; $this->namespaceBase = $namespaceBase;
return $this;
} }
/** /**
@ -41,12 +44,12 @@ class ModuleContextClassGuesser implements ClassGuesserInterface
public function guess() public function guess()
{ {
// Try fully qualified namespace // Try fully qualified namespace
if (class_exists($class = $this->namespace.'\\'.$this->classSuffix)) { if (class_exists($class = $this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) {
return $class; return $class;
} }
// Fall back to namespace with SilverStripe prefix // Fall back to namespace with SilverStripe prefix
// TODO Remove once core has namespace capabilities for modules // TODO Remove once core has namespace capabilities for modules
if (class_exists($class = 'SilverStripe\\'.$this->namespace.'\\'.$this->classSuffix)) { if (class_exists($class = 'SilverStripe\\'.$this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) {
return $class; return $class;
} }
} }

View File

@ -65,7 +65,9 @@ class Extension implements ExtensionInterface
*/ */
public function getCompilerPasses() public function getCompilerPasses()
{ {
return array(); return array(
new Compiler\CoreInitializationPass()
);
} }
/** /**

View File

@ -2,7 +2,8 @@ parameters:
behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer
behat.silverstripe_extension.context.class_guesser.class: SilverStripe\BehatExtension\Context\ClassGuesser\ModuleContextClassGuesser behat.silverstripe_extension.context.class_guesser.class: SilverStripe\BehatExtension\Context\ClassGuesser\ModuleContextClassGuesser
behat.console.processor.locator.class: SilverStripe\BehatExtension\Console\Processor\LocatorProcessor behat.console.processor.locator.class: SilverStripe\BehatExtension\Console\Processor\LocatorProcessor
behat.silverstripe_extension.context.class_suffix: Test\Behaviour\FeatureContext behat.console.processor.init.class: SilverStripe\BehatExtension\Console\Processor\InitProcessor
behat.silverstripe_extension.context.namespace_suffix: Test\Behaviour
behat.silverstripe_extension.framework_path: framework behat.silverstripe_extension.framework_path: framework
behat.silverstripe_extension.ajax_steps: ~ behat.silverstripe_extension.ajax_steps: ~
behat.silverstripe_extension.ajax_timeout: ~ behat.silverstripe_extension.ajax_timeout: ~
@ -27,6 +28,7 @@ services:
behat.silverstripe_extension.context.class_guesser: behat.silverstripe_extension.context.class_guesser:
class: %behat.silverstripe_extension.context.class_guesser.class% class: %behat.silverstripe_extension.context.class_guesser.class%
arguments: arguments:
- %behat.silverstripe_extension.context.class_suffix% - %behat.silverstripe_extension.context.namespace_suffix%
- %behat.context.class%
tags: tags:
- { name: behat.context.class_guesser, priority: 10 } - { name: behat.context.class_guesser, priority: 10 }