mirror of
https://github.com/silverstripe/silverstripe-behat-extension
synced 2024-10-22 17:05:32 +02:00
NEW FixtureContext (#2)
This commit is contained in:
parent
de60775b98
commit
30ece1347f
88
README.md
88
README.md
@ -141,53 +141,67 @@ To find out all available steps (and the files they are defined in), run the fol
|
||||
Note: There are more specific step definitions in the SilverStripe `framework` module
|
||||
for interacting with the CMS interfaces (see `framework/tests/behat/features/bootstrap`).
|
||||
|
||||
### Fixtures
|
||||
## Fixtures
|
||||
|
||||
Fixtures should be provided in YAML format (standard SilverStripe fixture format)
|
||||
as [PyStrings](http://docs.behat.org/guides/1.gherkin.html#pystrings)
|
||||
Since each test run creates a new database, you can't rely on existing state unless
|
||||
you explicitly define it.
|
||||
|
||||
Take a look at the sample fixture logic first:
|
||||
### Database Defaults
|
||||
|
||||
Given there are the following Permission records
|
||||
"""
|
||||
admin:
|
||||
Code: ADMIN
|
||||
"""
|
||||
And there are the following Group records
|
||||
"""
|
||||
admingroup:
|
||||
Title: Admin Group
|
||||
Code: admin
|
||||
Permissions: =>Permission.admin
|
||||
"""
|
||||
And there are the following Member records
|
||||
"""
|
||||
admin:
|
||||
FirstName: Admin
|
||||
Email: admin@test.com
|
||||
Groups: =>Group.admingroup
|
||||
"""
|
||||
The easiest way to get default data is through `DataObject->requireDefaultRecords()`.
|
||||
Many modules already have this method defined, e.g. the `blog` module automatically
|
||||
creates a default `BlogHolder` entry in the page tree. Sometimes these defaults can
|
||||
be counterproductive though, so you need to "opt-in" to them, via the `@database-defaults`
|
||||
tag placed at the top of your feature definition. The defaults are reset after each
|
||||
scenario automatically.
|
||||
|
||||
In this example, the fixture is used to create Admin member with admin permissions.
|
||||
### Inline Definition
|
||||
|
||||
As you can see, there are special Gherkin steps that take care of loading
|
||||
fixtures into database. They use the following format:
|
||||
If you need more flexibility and transparency about which records are being created,
|
||||
use the inline definition syntax. The following example shows some syntax variations:
|
||||
|
||||
Given there are the following TableName records
|
||||
"""
|
||||
RowIdentifier:
|
||||
ColumnName: Value
|
||||
"""
|
||||
Feature: Do something with pages
|
||||
As an site owner
|
||||
I want to manage pages
|
||||
|
||||
Fixtures may also use a `=>` symbol to indicate relationships between records.
|
||||
In the example above `=>Permission.admin` will be replaced with row `ID` of a
|
||||
`Permission` record that has `RowIdentifier` set as `admin`.
|
||||
Background:
|
||||
# Creates a new page without data. Can be accessed later under this identifier
|
||||
Given a page "Page 1"
|
||||
# Uses a custom RegistrationPage type
|
||||
And a registration page "Register"
|
||||
# Creates a page with inline properties
|
||||
And a page "Page 2" with "URLSegment"="page-1" and "Content"="my page 1"
|
||||
# Field names can be tabular, and based on DataObject::$field_labels
|
||||
And the page "Page 3" has the following data
|
||||
| Content | <blink> |
|
||||
| My Property | foo |
|
||||
| My Boolean | bar |
|
||||
# Pages are published by default, can be explicitly unpublished
|
||||
And the page "Page 1" is not published
|
||||
# Create a hierarchy, and reference a record created earlier
|
||||
And the page "Page 1.1" is a child of a page "Page 1"
|
||||
# Specific page type step
|
||||
And a page "My Redirect" which redirects to a page "Page 1"
|
||||
And a member "Website User" with "FavouritePage"="=>Page.Page 1"
|
||||
|
||||
Fixtures are created where you defined them. If you want the fixtures to be created
|
||||
before every scenario, define them in [Background](http://docs.behat.org/guides/1.gherkin.html#backgrounds). If you want them to be created only when a particular scenario runs, define them there.
|
||||
@javascript
|
||||
Scenario: View a page in the tree
|
||||
Given I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
Then I should see "Page 1" in CMS Tree
|
||||
|
||||
Fixtures are usually not cleared between scenarios. You can alter this behaviour
|
||||
* Fixtures are created where you defined them. If you want the fixtures to be created
|
||||
before every scenario, define them in [Background](http://docs.behat.org/guides/1.gherkin.html#backgrounds).
|
||||
If you want them to be created only when a particular scenario runs, define them there.
|
||||
* The basic syntax works for all `DataObject` subclasses, but some specific
|
||||
notations like "is not published" requires extensions like `Hierarchy` to be applied to the class
|
||||
* Record identifiers, property names and property values need to be quoted
|
||||
* Record types shouldn't be quoted, and can use more natural notation ("registration page" instead of "Registration Page")
|
||||
* Fixtures are usually not cleared between scenarios. You can alter this behaviour
|
||||
by tagging the feature or scenario with `@database-defaults` tag.
|
||||
* Property values may also use a `=>` symbol to indicate relationships between records.
|
||||
The notation is `=><classname>.<identifier>`. For `has_many` or `many_many` relationships,
|
||||
multiple relationships can be separated by a comma.
|
||||
|
||||
The module runner empties the database before each scenario tagged with
|
||||
`@database-defaults` and populates it with default records (usually a set of
|
||||
|
226
src/SilverStripe/BehatExtension/Context/FixtureContext.php
Normal file
226
src/SilverStripe/BehatExtension/Context/FixtureContext.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\BehatExtension\Context;
|
||||
|
||||
use Behat\Behat\Context\ClosuredContextInterface,
|
||||
Behat\Behat\Context\TranslatedContextInterface,
|
||||
Behat\Behat\Context\BehatContext,
|
||||
Behat\Behat\Context\Step,
|
||||
Behat\Behat\Event\StepEvent,
|
||||
Behat\Behat\Exception\PendingException;
|
||||
use Behat\Mink\Driver\Selenium2Driver;
|
||||
use Behat\Gherkin\Node\PyStringNode,
|
||||
Behat\Gherkin\Node\TableNode;
|
||||
|
||||
// PHPUnit
|
||||
require_once 'PHPUnit/Autoload.php';
|
||||
require_once 'PHPUnit/Framework/Assert/Functions.php';
|
||||
|
||||
/**
|
||||
* Context used to create fixtures in the SilverStripe ORM.
|
||||
*/
|
||||
class FixtureContext extends BehatContext
|
||||
{
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \FixtureFactory
|
||||
*/
|
||||
protected $fixtureFactory;
|
||||
|
||||
protected $filesPath;
|
||||
|
||||
protected $createdFilesPaths;
|
||||
|
||||
public function __construct(array $parameters)
|
||||
{
|
||||
$this->context = $parameters;
|
||||
}
|
||||
|
||||
public function getSession($name = null)
|
||||
{
|
||||
return $this->getMainContext()->getSession($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \FixtureFactory
|
||||
*/
|
||||
public function getFixtureFactory() {
|
||||
if(!$this->fixtureFactory) {
|
||||
$this->fixtureFactory = \Injector::inst()->create('FixtureFactory', 'FixtureContextFactory');
|
||||
}
|
||||
return $this->fixtureFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \FixtureFactory $factory
|
||||
*/
|
||||
public function setFixtureFactory(\FixtureFactory $factory) {
|
||||
$this->fixtureFactory = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Given a page "Page 1"
|
||||
*
|
||||
* @Given /^(?:(an|a|the) )(?<type>[^"]+)"(?<id>[^"]+)"$/
|
||||
*/
|
||||
public function stepCreateRecord($type, $id)
|
||||
{
|
||||
$class = $this->convertTypeToClass($type);
|
||||
$this->fixtureFactory->createObject($class, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Given a page "Page 1" with "URL"="page-1" and "Content"="my page 1"
|
||||
*
|
||||
* @Given /^(?:(an|a|the) )(?<type>[^"]+)"(?<id>[^"]+)" with (?<data>.*)$/
|
||||
*/
|
||||
public function stepCreateRecordWithData($type, $id, $data)
|
||||
{
|
||||
$class = $this->convertTypeToClass($type);
|
||||
preg_match_all(
|
||||
'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
|
||||
$data,
|
||||
$matches
|
||||
);
|
||||
$fields = $this->convertFields(
|
||||
$class,
|
||||
array_combine($matches['key'], $matches['value'])
|
||||
);
|
||||
$this->fixtureFactory->createObject($class, $id, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: And the page "Page 2" has the following data
|
||||
* | Content | <blink> |
|
||||
* | My Property | foo |
|
||||
* | My Boolean | bar |
|
||||
*
|
||||
* @Given /^(?:(an|a|the) )(?<type>[^"]+)"(?<id>[^"]+)" has the following data$/
|
||||
*/
|
||||
public function stepCreateRecordWithTable($type, $id, $null, TableNode $fieldsTable)
|
||||
{
|
||||
$class = $this->convertTypeToClass($type);
|
||||
// TODO Support more than one record
|
||||
$fields = $this->convertFields($class, $fieldsTable->getRowsHash());
|
||||
$this->fixtureFactory->createObject($class, $id, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Given the page "Page 1.1" is a child of the page "Page1"
|
||||
*
|
||||
* @Given /^(?:(an|a|the) )(?<type>[^"]+)"(?<id>[^"]+)" is a (?<relation>[^\s]*) of (?:(an|a|the) )(?<relationType>[^"]+)"(?<relationId>[^"]+)"/
|
||||
*/
|
||||
public function stepUpdateRecordRelation($type, $id, $relation, $relationType, $relationId)
|
||||
{
|
||||
$class = $this->convertTypeToClass($type);
|
||||
$relationClass = $this->convertTypeToClass($relationType);
|
||||
|
||||
$obj = $this->fixtureFactory->get($class, $id);
|
||||
if(!$obj) $obj = $this->fixtureFactory->createObject($class, $id);
|
||||
|
||||
$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
|
||||
if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
|
||||
|
||||
switch($relation) {
|
||||
case 'parent':
|
||||
$relationObj->ParentID = $obj->ID;
|
||||
$relationObj->write();
|
||||
break;
|
||||
case 'child':
|
||||
$obj->ParentID = $relationObj->ID;
|
||||
$obj->write();
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid relation "%s"', $relation
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Given the page "Page 1" is not published
|
||||
*
|
||||
* @Given /^(?:(an|a|the) )(?<type>[^"]+)"(?<id>[^"]+)" is (?<state>[^"]*)$/
|
||||
*/
|
||||
public function stepUpdateRecordState($type, $id, $state)
|
||||
{
|
||||
$class = $this->convertTypeToClass($type);
|
||||
$obj = $this->fixtureFactory->get($class, $id);
|
||||
if(!$obj) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Can not find record "%s" with identifier "%s"',
|
||||
$type,
|
||||
$id
|
||||
));
|
||||
}
|
||||
|
||||
switch($state) {
|
||||
case 'published':
|
||||
$obj->publish('Stage', 'Live');
|
||||
break;
|
||||
case 'not published':
|
||||
case 'unpublished':
|
||||
$oldMode = \Versioned::get_reading_mode();
|
||||
\Versioned::reading_stage('Live');
|
||||
$clone = clone $obj;
|
||||
$clone->delete();
|
||||
\Versioned::reading_stage($oldMode);
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid state: "%s"', $state
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a natural language class description to an actual class name.
|
||||
* Respects {@link DataObject::$singular_name} variations.
|
||||
* Example: "redirector page" -> "RedirectorPage"
|
||||
*
|
||||
* @param String
|
||||
* @return String Class name
|
||||
*/
|
||||
protected function convertTypeToClass($type)
|
||||
{
|
||||
$type = trim($type);
|
||||
|
||||
// Try direct mapping
|
||||
$class = str_replace(' ', '', ucfirst($type));
|
||||
if(class_exists($class) || !is_subclass_of($class, 'DataObject')) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
// Fall back to singular names
|
||||
foreach(array_values(\ClassInfo::subclassesFor('DataObject')) as $candidate) {
|
||||
if(singleton($candidate)->singular_name() == $type) return $candidate;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Class "%s" does not exist, or is not a subclass of DataObjet',
|
||||
$class
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an object with values, resolving aliases set through
|
||||
* {@link DataObject->fieldLabels()}.
|
||||
*
|
||||
* @param String Class name
|
||||
* @param Array Map of field names or aliases to their values.
|
||||
* @return Array Map of actual object properties to their values.
|
||||
*/
|
||||
protected function convertFields($class, $fields) {
|
||||
$labels = singleton($class)->fieldLabels();
|
||||
foreach($fields as $fieldName => $fieldVal) {
|
||||
if(array_key_exists($fieldName, $labels)) {
|
||||
unset($fields[$fieldName]);
|
||||
$fields[$labels[$fieldName]] = $fieldVal;
|
||||
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user