From 0f8b229d6b8a9de9577b03897a0189b75f04f6fc Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 11 Dec 2012 15:43:30 +1300 Subject: [PATCH 1/2] Modify Composer install path in docs Composer examples at http://getcomposer.org/doc/00-intro.md use /usr/local/bin for installing Composer globally, so let's use that path as well instead of /usr/bin. --- docs/en/installation/composer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/installation/composer.md b/docs/en/installation/composer.md index b8184b626..c738cc8c6 100644 --- a/docs/en/installation/composer.md +++ b/docs/en/installation/composer.md @@ -18,7 +18,7 @@ To install Composer, run the following commands from your command-line. curl -s https://getcomposer.org/installer | php # Move to your path - sudo mv composer.phar /usr/bin/composer + sudo mv composer.phar /usr/local/bin/composer Or [download composer.phar](http://getcomposer.org/composer.phar) manually, and rename `composer.phar` as `composer`, and put it in your path. On Windows, you should call the file `composer.bat`. From e8fbfc0bd1cfd2f480535df591967be990d822a3 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 7 Dec 2012 18:44:00 +0100 Subject: [PATCH 2/2] NEW FixtureFactory separated out from YamlFixture Enables more generic use of the fixture facilities without dependency on the YAML format, for example when creating fixtures from Behat step definitions. Note: The YamlFixture class needs to be created via Injector::inst()->create('YamlFixture') now, direct instantiation is no longer supported. --- dev/FixtureBlueprint.php | 267 ++++++++++++++++++ dev/FixtureFactory.php | 226 +++++++++++++++ dev/SapphireTest.php | 115 ++++---- dev/TestRunner.php | 6 +- dev/YamlFixture.php | 182 ++++-------- .../testing/create-silverstripe-test.md | 76 +---- docs/en/topics/testing/fixtures.md | 228 +++++++++++++++ tests/dev/FixtureBlueprintTest.php | 181 ++++++++++++ tests/dev/FixtureFactoryTest.php | 170 +++++++++++ tests/model/DataObjectTest.php | 4 +- tests/testing/YamlFixtureTest.php | 48 +++- tests/testing/YamlFixtureTest.yml | 25 +- 12 files changed, 1224 insertions(+), 304 deletions(-) create mode 100644 dev/FixtureBlueprint.php create mode 100644 dev/FixtureFactory.php create mode 100644 docs/en/topics/testing/fixtures.md create mode 100644 tests/dev/FixtureBlueprintTest.php create mode 100644 tests/dev/FixtureFactoryTest.php diff --git a/dev/FixtureBlueprint.php b/dev/FixtureBlueprint.php new file mode 100644 index 000000000..0b01458d6 --- /dev/null +++ b/dev/FixtureBlueprint.php @@ -0,0 +1,267 @@ + array(), + 'afterCreate' => array(), + ); + + static $dependencies = array( + 'factory' => '%$FixtureFactory' + ); + + /** + * @param String $name + * @param String $class Defaults to $name + * @param array $defaults + */ + public function __construct($name, $class = null, $defaults = array()) { + if(!$class) $class = $name; + + if(!is_subclass_of($class, 'DataObject')) { + throw new InvalidArgumentException(sprintf( + 'Class "%s" is not a valid subclass of DataObject', + $class + )); + } + + $this->name = $name; + $this->class = $class; + $this->defaults = $defaults; + } + + /** + * @param String $identifier Unique identifier for this fixture type + * @param Array $data Map of property names to their values. + * @param Array $fixtures Map of fixture names to an associative array of their in-memory + * identifiers mapped to their database IDs. Used to look up + * existing fixtures which might be referenced in the $data attribute + * via the => notation. + * @return DataObject + */ + public function createObject($identifier, $data = null, $fixtures = null) { + // We have to disable validation while we import the fixtures, as the order in + // which they are imported doesnt guarantee valid relations until after the import is complete. + $validationenabled = DataObject::get_validation_enabled(); + DataObject::set_validation_enabled(false); + + $this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures)); + + try { + $class = $this->class; + $obj = DataModel::inst()->$class->newObject(); + + // If an ID is explicitly passed, then we'll sort out the initial write straight away + // This is just in case field setters triggered by the population code in the next block + // Call $this->write(). (For example, in FileTest) + if(isset($data['ID'])) { + $obj->ID = $data['ID']; + + // The database needs to allow inserting values into the foreign key column (ID in our case) + $conn = DB::getConn(); + if(method_exists($conn, 'allowPrimaryKeyEditing')) { + $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true); + } + $obj->write(false, true); + if(method_exists($conn, 'allowPrimaryKeyEditing')) { + $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), false); + } + } + + // Populate defaults + if($this->defaults) foreach($this->defaults as $fieldName => $fieldVal) { + if(isset($data[$fieldName]) && $data[$fieldName] !== false) continue; + + if(is_callable($fieldVal)) { + $obj->$fieldName = $fieldVal($obj, $data, $fixtures); + } else { + $obj->$fieldName = $fieldVal; + } + } + + // Populate overrides + if($data) foreach($data as $fieldName => $fieldVal) { + // Defer relationship processing + if($obj->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($fieldName)) { + continue; + } + + $this->setValue($obj, $fieldName, $fieldVal, $fixtures); + } + $obj->write(); + + // Save to fixture before relationship processing in case of reflexive relationships + if(!isset($fixtures[$class])) { + $fixtures[$class] = array(); + } + $fixtures[$class][$identifier] = $obj->ID; + + // Populate all relations + if($data) foreach($data as $fieldName => $fieldVal) { + if($obj->many_many($fieldName) || $obj->has_many($fieldName)) { + $parsedItems = array(); + $items = preg_split('/ *, */',trim($fieldVal)); + foreach($items as $item) { + // Check for correct format: =>. + if(!preg_match('/^=>[^\.]+\.[^\.]+/', $item)) { + throw new InvalidArgumentException(sprintf( + 'Invalid format for relation "%s" on class "%s" ("%s")', + $fieldName, + $class, + $item + )); + } + + $parsedItems[] = $this->parseValue($item, $fixtures); + } + $obj->write(); + if($obj->has_many($fieldName)) { + $obj->getComponents($fieldName)->setByIDList($parsedItems); + } elseif($obj->many_many($fieldName)) { + $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems); + } + } elseif($obj->has_one($fieldName)) { + // Sets has_one with relation name + $obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures); + } elseif($obj->has_one(preg_replace('/ID$/', '', $fieldName))) { + // Sets has_one with database field + $obj->$fieldName = $this->parseValue($fieldVal, $fixtures); + } + } + $obj->write(); + + // If LastEdited was set in the fixture, set it here + if($data && array_key_exists('LastEdited', $data)) { + $edited = $this->parseValue($data['LastEdited'], $fixtures); + DB::manipulate(array( + $class => array( + "command" => "update", "id" => $obj->id, + "fields" => array("LastEdited" => "'".$edited."'") + ) + )); + } + } catch(Exception $e) { + DataObject::set_validation_enabled($validationenabled); + throw $e; + } + + DataObject::set_validation_enabled($validationenabled); + + $this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures)); + + return $obj; + } + + /** + * @param Array $defaults + */ + public function setDefaults($defaults) { + $this->defaults = $defaults; + return $this; + } + + /** + * @return Array + */ + public function getDefaults() { + return $this->defaults; + } + + /** + * @return String + */ + public function getClass() { + return $this->class; + } + + /** + * See class documentation. + * + * @param String $type + * @param callable $callback + */ + public function addCallback($type, $callback) { + if(!array_key_exists($type, $this->callbacks)) { + throw new InvalidArgumentException(sprintf('Invalid type "%s"', $type)); + } + + $this->callbacks[$type][] = $callback; + return $this; + } + + /** + * @param String $type + * @param callable $callback + */ + public function removeCallback($type, $callback) { + $pos = array_search($callback, $this->callbacks[$type]); + if($pos !== false) unset($this->callbacks[$type][$pos]); + + return $this; + } + + protected function invokeCallbacks($type, $args = array()) { + foreach($this->callbacks[$type] as $callback) { + call_user_func_array($callback, $args); + } + } + + /** + * Parse a value from a fixture file. If it starts with => + * it will get an ID from the fixture dictionary + * + * @param String $fieldVal + * @param Array $fixtures See {@link createObject()} + * @return String Fixture database ID, or the original value + */ + protected function parseValue($value, $fixtures = null) { + if(substr($value,0,2) == '=>') { + // Parse a dictionary reference - used to set foreign keys + list($class, $identifier) = explode('.', substr($value,2), 2); + + if($fixtures && !isset($fixtures[$class][$identifier])) { + throw new InvalidArgumentException(sprintf( + 'No fixture definitions found for "%s"', + $value + )); + } + + return $fixtures[$class][$identifier]; + } else { + // Regular field value setting + return $value; + } + } + + protected function setValue($obj, $name, $value, $fixtures = null) { + $obj->$name = $this->parseValue($value, $fixtures); + } + +} \ No newline at end of file diff --git a/dev/FixtureFactory.php b/dev/FixtureFactory.php new file mode 100644 index 000000000..7f206e68c --- /dev/null +++ b/dev/FixtureFactory.php @@ -0,0 +1,226 @@ + + * $factory = new FixtureFactory(); + * $relatedObj = $factory->createObject( + * 'MyRelatedClass', + * 'relation1' + * ); + * $obj = $factory->createObject( + * 'MyClass', + * 'object1' + * array('MyRelationName' => '=>MyRelatedClass.relation1') + * ); + * + * Relation loading is order dependant. + * + * @package framework + * @subpackage core + */ +class FixtureFactory { + + /** + * @var array Array of fixture items, keyed by class and unique identifier, + * with values being the generated database ID. Does not store object instances. + */ + protected $fixtures = array(); + + /** + * @var array Callbacks + */ + protected $blueprints = array(); + + /** + * @param String $name Unique name for this blueprint + * @param array|FixtureBlueprint $defaults Array of default values, or a blueprint instance + */ + public function define($name, $defaults = array()) { + if($defaults instanceof FixtureBlueprint) { + $this->blueprints[$name] = $defaults; + } else { + $class = $name; + $this->blueprints[$name] = Injector::inst()->create( + 'FixtureBlueprint', $name, $class, $defaults + ); + } + + return $this; + } + + /** + * Writes the fixture into the database using DataObjects + * + * @param String $name Name of the {@link FixtureBlueprint} to use, + * usually a DataObject subclass. + * @param String $identifier Unique identifier for this fixture type + * @param Array $data Map of properties. Overrides default data. + * @return DataObject + */ + public function createObject($name, $identifier, $data = null) { + if(!isset($this->blueprints[$name])) { + $this->blueprints[$name] = new FixtureBlueprint($name); + } + $blueprint = $this->blueprints[$name]; + $obj = $blueprint->createObject($identifier, $data, $this->fixtures); + $class = $blueprint->getClass(); + + if(!isset($this->fixtures[$class])) { + $this->fixtures[$class] = array(); + } + $this->fixtures[$class][$identifier] = $obj->ID; + + return $obj; + } + + /** + * Writes the fixture into the database directly using a database manipulation. + * Does not use blueprints. Only supports tables with a primary key. + * + * @param String $table Existing database table name + * @param String $identifier Unique identifier for this fixture type + * @param Array $data Map of properties + * @return Int Database identifier + */ + public function createRaw($table, $identifier, $data) { + $manipulation = array($table => array("fields" => array(), "command" => "insert")); + foreach($data as $fieldName => $fieldVal) { + $manipulation[$table]["fields"][$fieldName] = "'" . $this->parseValue($fieldVal) . "'"; + } + DB::manipulate($manipulation); + $id = DB::getGeneratedID($table); + $this->fixtures[$table][$identifier] = $id; + + return $id; + } + + /** + * Get the ID of an object from the fixture. + * @param $className The data class, as specified in your fixture file. Parent classes won't work + * @param $identifier The identifier string, as provided in your fixture file + */ + public function getId($class, $identifier) { + if(isset($this->fixtures[$class][$identifier])) { + return $this->fixtures[$class][$identifier]; + } else { + return false; + } + } + + /** + * Return all of the IDs in the fixture of a particular class name. + * + * @return A map of fixture-identifier => object-id + */ + public function getIds($class) { + if(isset($this->fixtures[$class])) { + return $this->fixtures[$class]; + } else { + return false; + } + } + + /** + * @param String + * @param String $identifier + * @param Int $databaseId + */ + public function setId($class, $identifier, $databaseId) { + $this->fixtures[$class][$identifier] = $databaseId; + return $this; + } + + /** + * Get an object from the fixture. + * + * @param $class The data class, as specified in your fixture file. Parent classes won't work + * @param $identifier The identifier string, as provided in your fixture file + */ + public function get($class, $identifier) { + $id = $this->getId($class, $identifier); + if($id) return DataObject::get_by_id($class, $id); + } + + /** + * @return Array Map of class names, containing a map of in-memory identifiers + * mapped to database identifiers. + */ + public function getFixtures() { + return $this->fixtures; + } + + /** + * Remove all fixtures previously defined through {@link createObject()} + * or {@link createRaw()}, both from the internal fixture mapping and the database. + * If the $class argument is set, limit clearing to items of this class. + * + * @param String $class + */ + public function clear($limitToClass = null) { + $classes = ($limitToClass) ? array($limitToClass) : array_keys($this->fixtures); + foreach($classes as $class) { + $ids = $this->fixtures[$class]; + foreach($ids as $id => $dbId) { + if(class_exists($class)) { + $class::get()->byId($dbId)->delete(); + } else { + $table = $class; + DB::manipulate(array( + $table => array("fields" => array('ID' => $dbId), + "command" => "delete") + )); + } + + unset($this->fixtures[$class][$id]); + } + } + } + + /** + * @return Array Of {@link FixtureBlueprint} instances + */ + public function getBlueprints() { + return $this->blueprints; + } + + /** + * @param String $name + * @return FixtureBlueprint + */ + public function getBlueprint($name) { + return (isset($this->blueprints[$name])) ? $this->blueprints[$name] : false; + } + + /** + * Parse a value from a fixture file. If it starts with => + * it will get an ID from the fixture dictionary + * + * @param String $fieldVal + * @return String Fixture database ID, or the original value + */ + protected function parseValue($value) { + if(substr($value,0,2) == '=>') { + // Parse a dictionary reference - used to set foreign keys + list($class, $identifier) = explode('.', substr($value,2), 2); + + if($this->fixtures && !isset($this->fixtures[$class][$identifier])) { + throw new InvalidArgumentException(sprintf( + 'No fixture definitions found for "%s"', + $value + )); + } + + return $this->fixtures[$class][$identifier]; + } else { + // Regular field value setting + return $value; + } + } + +} \ No newline at end of file diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 531508ff7..463b3ea46 100644 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -10,6 +10,11 @@ require_once 'TestRunner.php'; * @subpackage testing */ class SapphireTest extends PHPUnit_Framework_TestCase { + + static $dependencies = array( + 'fixtureFactory' => '%$FixtureFactory', + ); + /** * Path to fixture data for this test run. * If passed as an array, multiple fixture files will be loaded. @@ -19,11 +24,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase { * @var string|array */ static $fixture_file = null; + + /** + * @var FixtureFactory + */ + protected $fixtureFactory; /** - * Set whether to include this test in the TestRunner or to skip this. - * - * @var bool + * @var bool Set whether to include this test in the TestRunner or to skip this. */ protected $skipTest = false; @@ -140,8 +148,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase { /** * @var array $fixtures Array of {@link YamlFixture} instances + * @deprecated 3.1 Use $fixtureFactory instad */ - protected $fixtures; + protected $fixtures = array(); protected $model; @@ -234,8 +243,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase { if($resolvedPath) $fixtureFilePath = $resolvedPath; } - $fixture = new YamlFixture($fixtureFilePath); - $fixture->saveIntoDatabase($this->model); + $fixture = Injector::inst()->create('YamlFixture', $fixtureFilePath); + $fixture->writeInto($this->getFixtureFactory()); $this->fixtures[] = $fixture; // backwards compatibility: Load first fixture into $this->fixture @@ -339,38 +348,37 @@ class SapphireTest extends PHPUnit_Framework_TestCase { } /** - * Array + * @return FixtureFactory */ - protected $fixtureDictionary; - + public function getFixtureFactory() { + if(!$this->fixtureFactory) $this->fixtureFactory = Injector::inst()->create('FixtureFactory'); + return $this->fixtureFactory; + } + + public function setFixtureFactory(FixtureFactory $factory) { + $this->fixtureFactory = $factory; + return $this; + } /** * Get the ID of an object from the fixture. + * * @param $className The data class, as specified in your fixture file. Parent classes won't work * @param $identifier The identifier string, as provided in your fixture file * @return int */ protected function idFromFixture($className, $identifier) { - if(!$this->fixtures) { - user_error("You've called idFromFixture() but you haven't specified static \$fixture_file.\n", - E_USER_WARNING); - return; - } - - foreach($this->fixtures as $fixture) { - $match = $fixture->idFromFixture($className, $identifier); - if($match) return $match; + $id = $this->getFixtureFactory()->getId($className, $identifier); + + if(!$id) { + user_error(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + ), E_USER_ERROR); } - $fixtureFiles = Config::inst()->get(get_class($this), 'fixture_file', Config::FIRST_SET); - user_error(sprintf( - "Couldn't find object '%s' (class: %s) in files %s", - $identifier, - $className, - (is_array($fixtureFiles)) ? implode(',', $fixtureFiles) : $fixtureFiles - ), E_USER_ERROR); - - return false; + return $id; } /** @@ -381,46 +389,27 @@ class SapphireTest extends PHPUnit_Framework_TestCase { * @return A map of fixture-identifier => object-id */ protected function allFixtureIDs($className) { - if(!$this->fixtures) { - user_error("You've called allFixtureIDs() but you haven't specified static \$fixture_file.\n", - E_USER_WARNING); - return; - } - - $ids = array(); - foreach($this->fixtures as $fixture) { - $ids += $fixture->allFixtureIDs($className); - } - - return $ids; + return $this->getFixtureFactory()->getIds($className); } /** * Get an object from the fixture. + * * @param $className The data class, as specified in your fixture file. Parent classes won't work * @param $identifier The identifier string, as provided in your fixture file */ protected function objFromFixture($className, $identifier) { - if(!$this->fixtures) { - user_error("You've called objFromFixture() but you haven't specified static \$fixture_file.\n", - E_USER_WARNING); - return; - } - - foreach($this->fixtures as $fixture) { - $match = $fixture->objFromFixture($className, $identifier); - if($match) return $match; - } + $obj = $this->getFixtureFactory()->get($className, $identifier); - $fixtureFiles = Config::inst()->get(get_class($this), 'fixture_file', Config::FIRST_SET); - user_error(sprintf( - "Couldn't find object '%s' (class: %s) in files %s", - $identifier, - $className, - (is_array($fixtureFiles)) ? implode(',', $fixtureFiles) : $fixtureFiles - ), E_USER_ERROR); + if(!$obj) { + user_error(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + ), E_USER_ERROR); + } - return false; + return $obj; } /** @@ -431,20 +420,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase { * @param $fixtureFile The location of the .yml fixture file, relative to the site base dir */ public function loadFixture($fixtureFile) { - $parser = new Spyc(); - $fixtureContent = $parser->load(Director::baseFolder().'/'.$fixtureFile); - - $fixture = new YamlFixture($fixtureFile); - $fixture->saveIntoDatabase($this->model); + $fixture = Injector::inst()->create('YamlFixture', $fixtureFile); + $fixture->writeInto($this->getFixtureFactory()); $this->fixtures[] = $fixture; } /** * Clear all fixtures which were previously loaded through - * {@link loadFixture()}. + * {@link loadFixture()} */ public function clearFixtures() { $this->fixtures = array(); + $this->getFixtureFactory()->clear(); } /** @@ -765,7 +752,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase { $dbConn->selectDatabase($dbname); $dbConn->createDatabase(); - $st = new SapphireTest(); + $st = Injector::inst()->create('SapphireTest'); $st->resetDBSchema(); // Reinstate PHPUnit error handling diff --git a/dev/TestRunner.php b/dev/TestRunner.php index 51fad63fb..44faddd50 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -392,7 +392,7 @@ HTML; // Fixture if($fixtureFile) { - $fixture = new YamlFixture($fixtureFile); + $fixture = Injector::inst()->create('YamlFixture', $fixtureFile); $fixture->saveIntoDatabase(); // If no fixture, then use defaults @@ -466,7 +466,7 @@ HTML; SapphireTest::empty_temp_db(); if(isset($_GET['fixture']) && ($fixtureFile = $_GET['fixture'])) { - $fixture = new YamlFixture($fixtureFile); + $fixture = Injector::inst()->create('YamlFixture', $fixtureFile); $fixture->saveIntoDatabase(); return "

Re-test the test database with fixture '$fixtureFile'. Time to start testing; where would" . " you like to start?

"; @@ -530,7 +530,7 @@ HTML; } // Fixture - $fixture = new YamlFixture($fixtureFile); + $fixture = Injector::inst()->create('YamlFixture', $fixtureFile); $fixture->saveIntoDatabase(); return "

Loaded fixture '$fixtureFile' into session

"; diff --git a/dev/YamlFixture.php b/dev/YamlFixture.php index fb7038c90..f8b1aaad2 100644 --- a/dev/YamlFixture.php +++ b/dev/YamlFixture.php @@ -66,11 +66,9 @@ require_once 'thirdparty/spyc/spyc.php'; * @subpackage core * * @see http://code.google.com/p/spyc/ - * - * @todo Write unit test for YamlFixture */ class YamlFixture extends Object { - + /** * Absolute path to the .yml fixture file * @@ -78,13 +76,6 @@ class YamlFixture extends Object { */ protected $fixtureFile; - /** - * Array of fixture items - * - * @var array - */ - protected $fixtureDictionary; - /** * String containing fixture * @@ -92,6 +83,12 @@ class YamlFixture extends Object { */ protected $fixtureString; + /** + * @var FixtureFactory + * @deprecated 3.1 Use writeInto() and FixtureFactory instead + */ + protected $factory; + /** * @param String Absolute file path, or relative path to {@link Director::baseFolder()} */ @@ -125,44 +122,50 @@ class YamlFixture extends Object { public function getFixtureString() { return $this->fixtureString; } - + /** * Get the ID of an object from the fixture. + * + * @deprecated 3.1 Use writeInto() and FixtureFactory accessors instead + * * @param $className The data class, as specified in your fixture file. Parent classes won't work * @param $identifier The identifier string, as provided in your fixture file */ public function idFromFixture($className, $identifier) { - if(isset($this->fixtureDictionary[$className][$identifier])) { - return $this->fixtureDictionary[$className][$identifier]; - } else { - return false; - } + Deprecation::notice('3.1', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->getId($className, $identifier); } /** * Return all of the IDs in the fixture of a particular class name. + * + * @deprecated 3.1 Use writeInto() and FixtureFactory accessors instead * * @return A map of fixture-identifier => object-id */ public function allFixtureIDs($className) { - if(isset($this->fixtureDictionary[$className])) { - return $this->fixtureDictionary[$className]; - } else { - return false; - } - + Deprecation::notice('3.1', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->getIds($className); } /** * Get an object from the fixture. + * + * @deprecated 3.1 Use writeInto() and FixtureFactory accessors instead * * @param $className The data class, as specified in your fixture file. Parent classes won't work * @param $identifier The identifier string, as provided in your fixture file */ public function objFromFixture($className, $identifier) { - $id = $this->idFromFixture($className, $identifier); - if($id) return DataObject::get_by_id($className, $id); + Deprecation::notice('3.1', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->get($className, $identifier); } /** @@ -172,14 +175,24 @@ class YamlFixture extends Object { * Caution: In order to support reflexive relations which need a valid object ID, * the record is written twice: first after populating all non-relational fields, * then again after populating all relations (has_one, has_many, many_many). + * + * @deprecated 3.1 Use writeInto() and FixtureFactory instance instead */ public function saveIntoDatabase(DataModel $model) { - // We have to disable validation while we import the fixtures, as the order in - // which they are imported doesnt guarantee valid relations until after the - // import is complete. - $validationenabled = DataObject::get_validation_enabled(); - DataObject::set_validation_enabled(false); - + Deprecation::notice('3.1', 'Use writeInto() and FixtureFactory instance instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + $this->writeInto($this->factory); + } + + /** + * Persists the YAML data in a FixtureFactory, + * which in turn saves them into the database. + * Please use the passed in factory to access the fixtures afterwards. + * + * @param FixtureFactory $factory + */ + public function writeInto(FixtureFactory $factory) { $parser = new Spyc(); if (isset($this->fixtureString)) { $fixtureContent = $parser->load($this->fixtureString); @@ -187,112 +200,15 @@ class YamlFixture extends Object { $fixtureContent = $parser->loadFile($this->fixtureFile); } - $this->fixtureDictionary = array(); - foreach($fixtureContent as $dataClass => $items) { - if(ClassInfo::exists($dataClass)) { - $this->writeDataObject($model, $dataClass, $items); - } else { - $this->writeSQL($dataClass, $items); - } - } - - DataObject::set_validation_enabled($validationenabled); - } - - /** - * Writes the fixture into the database using DataObjects - * - * @param string $dataClass - * @param array $items - */ - protected function writeDataObject($model, $dataClass, $items) { - foreach($items as $identifier => $fields) { - $obj = $model->$dataClass->newObject(); - - // If an ID is explicitly passed, then we'll sort out the initial write straight away - // This is just in case field setters triggered by the population code in the next block - // Call $this->write(). (For example, in FileTest) - if(isset($fields['ID'])) { - $obj->ID = $fields['ID']; - - // The database needs to allow inserting values into the foreign key column (ID in our case) - $conn = DB::getConn(); - if(method_exists($conn, 'allowPrimaryKeyEditing')) { - $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($dataClass), true); + foreach($fixtureContent as $class => $items) { + foreach($items as $identifier => $data) { + if(ClassInfo::exists($class)) { + $factory->createObject($class, $identifier, $data); + } else { + $factory->createRaw($class, $identifier, $data); } - $obj->write(false, true); - if(method_exists($conn, 'allowPrimaryKeyEditing')) { - $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($dataClass), false); - } - } - - // Populate the dictionary with the ID - if($fields) foreach($fields as $fieldName => $fieldVal) { - if($obj->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($fieldName)) continue; - $obj->$fieldName = $this->parseFixtureVal($fieldVal); - } - $obj->write(); - - // has to happen before relations in case a class is referring to itself - $this->fixtureDictionary[$dataClass][$identifier] = $obj->ID; - - // Populate all relations - if($fields) foreach($fields as $fieldName => $fieldVal) { - if($obj->many_many($fieldName) || $obj->has_many($fieldName)) { - $parsedItems = array(); - $items = preg_split('/ *, */',trim($fieldVal)); - foreach($items as $item) { - $parsedItems[] = $this->parseFixtureVal($item); - } - $obj->write(); - if($obj->has_many($fieldName)) { - $obj->getComponents($fieldName)->setByIDList($parsedItems); - } elseif($obj->many_many($fieldName)) { - $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems); - } - } elseif($obj->has_one($fieldName)) { - $obj->{$fieldName . 'ID'} = $this->parseFixtureVal($fieldVal); - } - } - $obj->write(); - //If LastEdited was set in the fixture, set it here - if (array_key_exists('LastEdited', $fields)) { - $manip = array($dataClass => array("command" => "update", "id" => $obj->id, - "fields" => array("LastEdited" => "'".$this->parseFixtureVal($fields['LastEdited'])."'"))); - DB::manipulate($manip); } } } - - /** - * Writes the fixture into the database directly using a database manipulation - * - * @param string $table - * @param array $items - */ - protected function writeSQL($table, $items) { - foreach($items as $identifier => $fields) { - $manipulation = array($table => array("fields" => array(), "command" => "insert")); - foreach($fields as $fieldName=> $fieldVal) { - $manipulation[$table]["fields"][$fieldName] = "'".$this->parseFixtureVal($fieldVal)."'"; - } - DB::manipulate($manipulation); - $this->fixtureDictionary[$table][$identifier] = DB::getGeneratedID($table); - } - } - - /** - * Parse a value from a fixture file. If it starts with => it will get an ID from the fixture dictionary - */ - protected function parseFixtureVal($fieldVal) { - // Parse a dictionary reference - used to set foreign keys - if(substr($fieldVal,0,2) == '=>') { - list($a, $b) = explode('.', substr($fieldVal,2), 2); - return $this->fixtureDictionary[$a][$b]; - // Regular field value setting - } else { - return $fieldVal; - } - } } diff --git a/docs/en/topics/testing/create-silverstripe-test.md b/docs/en/topics/testing/create-silverstripe-test.md index ea1bd86f3..a1f329ddc 100644 --- a/docs/en/topics/testing/create-silverstripe-test.md +++ b/docs/en/topics/testing/create-silverstripe-test.md @@ -62,76 +62,8 @@ specific to the framework, e.g. `[assertEmailSent](api:SapphireTest->assertEmail which can simulate sending emails through the `Email->send()` API without actually using a mail server (see the [testing emails](email-sending)) guide. -## The Database YAML file +## Fixtures -The main feature of `[api:SapphireTest]` over the raw PHPUnit classes is that SapphireTest will prepare a temporary database for -you. The content of that database is provided in a special YAML file. YAML is a simple markup languages that uses tabs -and colons instead of the more verbose XML tags, and because of this much better for developers creating files by hand. - -We will begin with a sample file and talk our way through it. - - Page: - home: - Title: Home - about: - Title: About Us - staff: - Title: Staff - URLSegment: my-staff - Parent: =>Page.about - staffduplicate: - Title: Staff - URLSegment: my-staff - Parent: =>Page.about - products: - Title: Products - product1: - Title: 1.1 Test Product - product2: - Title: Another Product - product3: - Title: Another Product - product4: - Title: Another Product - contact: - Title: Contact Us - - ErrorPage: - 404: - Title: Page not Found - ErrorCode: 404 - - -The contents of the YAML file are broken into three levels. - -* **Top level: class names** - Page and ErrorPage. This is the name of the dataobject class that should be created. -The fact that ErrorPage is actually a subclass is irrelevant to the system populating the database. It just -instantiates the object you specify. -* **Second level: identifiers** - home, about, staff, staffduplicate, etc. These are the identifiers that you pass as -the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record. -This means that every record needs to have an identifier, whether you use it or not. -* **Third level: fields** - each field for the record is listed as a 3rd level entry. In most cases, the field's raw -content is provided. However, if you want to define a relationship, you can do so using "=>". - -There are a couple of lines like this: - - Parent: =>Page.about - -This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about". -This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and -not the name of the database field (ParentID) - -On many-many relationships, you should specify a comma separated list of values. - - MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3 - -An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is -populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any -onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our -testURLGeneration() test above. - -For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the -fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2. - -Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify -standard pages such as 404 and home in your YAML file. +Often you need to test your functionality with some existing data, so called "fixtures". +These records are inserted on a fresh test database automatically. +[Read more about fixtures](fixtures). \ No newline at end of file diff --git a/docs/en/topics/testing/fixtures.md b/docs/en/topics/testing/fixtures.md new file mode 100644 index 000000000..fe3c148b9 --- /dev/null +++ b/docs/en/topics/testing/fixtures.md @@ -0,0 +1,228 @@ +# Fixtures + +## Overview + +Often you need to test your functionality with some existing data, so called "fixtures". +The `[api:SapphireTest]` class already prepares an empty database for you, +and you have various ways to define those fixtures. + +## YAML Fixtures + +YAML is a markup language which is deliberately simple and easy to read, +so ideal for our fixture generation. + +We will begin with a sample file and talk our way through it. + + Page: + home: + Title: Home + about: + Title: About Us + staff: + Title: Staff + URLSegment: my-staff + Parent: =>Page.about + + RedirectorPage: + redirect_home: + RedirectionType: Internal + LinkTo: =>Page.home + + +The contents of the YAML file are broken into three levels. + +* **Top level: class names** - `Page` and `RedirectorPage`. This is the name of the dataobject class that should be created. +The fact that `RedirectorPage` is actually a subclass is irrelevant to the system populating the database. It just +instantiates the object you specify. +* **Second level: identifiers** - `home`, `about`, etc. These are the identifiers that you pass as +the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record. +This means that every record needs to have an identifier, whether you use it or not. +* **Third level: fields** - each field for the record is listed as a 3rd level entry. In most cases, the field's raw +content is provided. However, if you want to define a relationship, you can do so using "=>". + +There are a couple of lines like this: + + Parent: =>Page.about + +This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about". +This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and +not the name of the database field (ParentID) + +On many-many relationships, you should specify a comma separated list of values. + + MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3 + +An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is +populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any +onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our +testURLGeneration() test above. + +For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the +fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2. + +Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify +standard pages such as 404 and home in your YAML file. + +## Test Class Definition + + + +## Manual Object Creation + +Sometimes statically defined fixtures don't suffice, because of the complexity of the tested model, +or because the YAML format doesn't allow you to modify all model state. +One common example here is publishing pages (page fixtures aren't published by default). + +You can always resort to creating objects manually in the test setup phase. +Since the test database is cleared on every test method, you'll get a fresh +set of test instances every time. + + :::php + class SiteTreeTest extends SapphireTest { + function setUp() { + parent::setUp(); + + for($i=0; $i<100; $i++) { + $page = new Page(array('Title' => "Page $i")); + $page->write(); + $page->publish('Stage', 'Live'); + } + } + } + +## Fixture Factories + +### Why Factories? + +Manually defined fixture provide full flexibility, but very little in terms of structure and convention. +Alternatively, you can use the `[api:FixtureFactory]` class, which allows you +to set default values, callbacks on object creation, and dynamic/lazy value setting. +By the way, the `SapphireTest` YAML fixtures rely on internally on this class as well. + +The idea is that rather than instanciating objects directly, we'll have a factory class for them. +This factory can have so called "blueprints" defined on it, which tells the factory +how to instanciate an object of a specific type. Blueprints need a name, +which is usually set to the class it creates. + +### Usage + +Since blueprints are auto-created for all available DataObject subclasses, +you only need to instanciate a factory to start using it. + + :::php + $factory = Injector::inst()->create('FixtureFactory'); + $obj = $factory->createObject('MyClass', 'myobj1'); + +It is important to remember that fixtures are referenced by arbitrary +identifiers ('myobj1'). These are internally mapped to their database identifiers. + + ::: + $databaseId = $factory->getId('MyClass', 'myobj1'); + +In order to create an object with certain properties, just add a second argument: + + :::php + $obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value')); + +### Default Properties + +Blueprints can be overwritten in order to customize their behaviour, +for example with default properties in case none are passed into `createObject()`. + + :::php + $factory->define('MyObject', array( + 'MyProperty' => 'My Default Value' + )); + +### Dependent Properties + +Values can be set on demand through anonymous functions, +which can either generate random defaults, or create +composite values based on other fixture data. + + :::php + $factory->define('Member', array( + 'Email' => function($obj, $data, $fixtures) { + if(isset($data['FirstName']) { + $obj->Email = strtolower($data['FirstName']) . '@example.org'; + } + }, + 'Score' => function($obj, $data, $fixtures) { + $obj->Score = rand(0,10); + } + )); + +### Relations + +Model relations can be expressed through the same notation as in the YAML fixture format +described earlier, through the `=>` prefix on data values. + + :::php + $obj = $factory->createObject('MyObject', array( + 'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2' + )); + +### Callbacks + +Sometimes new model instances need to be modified in ways which can't be expressed +in their properties, for example to publish a page, which requires a method call. + + :::php + $blueprint = Injector::inst()->create('FixtureBlueprint', 'Member'); + $blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { + $obj->publish('Stage', 'Live'); + }); + $page = $factory->define('Page', $blueprint); + +Available callbacks: + + * `beforeCreate($identifier, $data, $fixtures)` + * `afterCreate($obj, $identifier, $data, $fixtures)` + +### Multiple Blueprints + +Data of the same type can have variations, for example forum members vs. +CMS admins could both inherit from the `Member` class, but have completely +different properties. This is where named blueprints come in. +By default, blueprint names equal the class names they manage. + + :::php + $memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member'); + $adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member'); + $adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) { + if(isset($fixtures['Group']['admin'])) { + $adminGroup = Group::get()->byId($fixtures['Group']['admin']); + $obj->Groups()->add($adminGroup); + } + }); + + $member = $factory->createObject('Member'); // not in admin group + $admin = $factory->createObject('AdminMember'); // in admin group + +### Full Test Example + + :::php + class MyObjectTest extends SapphireTest { + + protected $factory; + + function __construct() { + parent::__construct(); + + $factory = Injector::inst()->create('FixtureFactory'); + // Defines a "blueprint" for new objects + $factory->define('MyObject', array( + 'MyProperty' => 'My Default Value' + )); + $this->factory = $factory; + } + + function testSomething() { + $MyObjectObj = $this->factory->createObject( + 'MyObject', + array('MyOtherProperty' => 'My Custom Value') + ); + // $myPageObj->MyProperty = My Default Value + // $myPageObj->MyOtherProperty = My Custom Value + } + } \ No newline at end of file diff --git a/tests/dev/FixtureBlueprintTest.php b/tests/dev/FixtureBlueprintTest.php new file mode 100644 index 000000000..a5613d85f --- /dev/null +++ b/tests/dev/FixtureBlueprintTest.php @@ -0,0 +1,181 @@ +createObject('one'); + $this->assertNotNull($obj); + $this->assertGreaterThan(0, $obj->ID); + $this->assertEquals('', $obj->Name); + } + + public function testCreateWithData() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + $obj = $blueprint->createObject('one', array('Name' => 'My Name')); + $this->assertNotNull($obj); + $this->assertGreaterThan(0, $obj->ID); + $this->assertEquals('My Name', $obj->Name); + } + + public function testCreateWithRelationship() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + + $relation1 = new FixtureFactoryTest_DataObjectRelation(); + $relation1->write(); + $relation2 = new FixtureFactoryTest_DataObjectRelation(); + $relation2->write(); + + $obj = $blueprint->createObject( + 'one', + array( + 'ManyMany' => + '=>FixtureFactoryTest_DataObjectRelation.relation1,' . + '=>FixtureFactoryTest_DataObjectRelation.relation2' + ), + array( + 'FixtureFactoryTest_DataObjectRelation' => array( + 'relation1' => $relation1->ID, + 'relation2' => $relation2->ID + ) + ) + ); + + $this->assertEquals(2, $obj->ManyMany()->Count()); + $this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID)); + $this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID)); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No fixture definitions found + */ + public function testCreateWithInvalidRelationName() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + + $obj = $blueprint->createObject( + 'one', + array( + 'ManyMany' => '=>UnknownClass.relation1' + ), + array( + 'FixtureFactoryTest_DataObjectRelation' => array( + 'relation1' => 99 + ) + ) + ); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No fixture definitions found + */ + public function testCreateWithInvalidRelationIdentifier() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + + $obj = $blueprint->createObject( + 'one', + array( + 'ManyMany' => '=>FixtureFactoryTest_DataObjectRelation.unknown_identifier' + ), + array( + 'FixtureFactoryTest_DataObjectRelation' => array( + 'relation1' => 99 + ) + ) + ); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid format + */ + public function testCreateWithInvalidRelationFormat() { + $factory = new FixtureFactory(); + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + + $relation1 = new FixtureFactoryTest_DataObjectRelation(); + $relation1->write(); + + $obj = $blueprint->createObject( + 'one', + array( + 'ManyMany' => 'FixtureFactoryTest_DataObjectRelation.relation1' + ), + array( + 'FixtureFactoryTest_DataObjectRelation' => array( + 'relation1' => $relation1->ID + ) + ) + ); + } + + public function testCreateWithId() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + $obj = $blueprint->createObject('ninetynine', array('ID' => 99)); + $this->assertNotNull($obj); + $this->assertEquals(99, $obj->ID); + } + + function testCallbackOnBeforeCreate() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + $this->_called = 0; + $self = $this; + $cb = function($identifier, $data, $fixtures) use($self) { + $self->_called = $self->_called + 1; + }; + $blueprint->addCallback('beforeCreate', $cb); + $this->assertEquals(0, $this->_called); + $obj1 = $blueprint->createObject('one'); + $this->assertEquals(1, $this->_called); + $obj2 = $blueprint->createObject('two'); + $this->assertEquals(2, $this->_called); + + $this->_called = 0; + } + + function testCallbackOnAfterCreate() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + $this->_called = 0; + $self = $this; + $cb = function($obj, $identifier, $data, $fixtures) use($self) { + $self->_called = $self->_called + 1; + }; + $blueprint->addCallback('afterCreate', $cb); + $this->assertEquals(0, $this->_called); + $obj1 = $blueprint->createObject('one'); + $this->assertEquals(1, $this->_called); + $obj2 = $blueprint->createObject('two'); + $this->assertEquals(2, $this->_called); + + $this->_called = 0; + } + + function testDefineWithDefaultCustomSetters() { + $blueprint = new FixtureBlueprint( + 'FixtureFactoryTest_DataObject', + null, + array( + 'Name' => function($obj, $data, $fixtures) { + return 'Default Name'; + } + )); + + $obj1 = $blueprint->createObject('one'); + $this->assertEquals('Default Name', $obj1->Name); + + $obj2 = $blueprint->createObject('one', array('Name' => 'Override Name')); + $this->assertEquals('Override Name', $obj2->Name); + } + +} \ No newline at end of file diff --git a/tests/dev/FixtureFactoryTest.php b/tests/dev/FixtureFactoryTest.php new file mode 100644 index 000000000..5e3a9fc41 --- /dev/null +++ b/tests/dev/FixtureFactoryTest.php @@ -0,0 +1,170 @@ +createRaw('FixtureFactoryTest_DataObject', 'one', array('Name' => 'My Name')); + $this->assertNotNull($id); + $this->assertGreaterThan(0, $id); + $obj = FixtureFactoryTest_DataObject::get()->find('ID', $id); + $this->assertNotNull($obj); + $this->assertEquals('My Name', $obj->Name); + } + + public function testSetId() { + $factory = new FixtureFactory(); + $obj = new FixtureFactoryTest_DataObject(); + $obj->write(); + $factory->setId('FixtureFactoryTest_DataObject', 'one', $obj->ID); + $this->assertEquals( + $obj->ID, + $factory->getId('FixtureFactoryTest_DataObject', 'one') + ); + } + + public function testGetId() { + $factory = new FixtureFactory(); + $obj = $factory->createObject('FixtureFactoryTest_DataObject', 'one'); + $this->assertEquals( + $obj->ID, + $factory->getId('FixtureFactoryTest_DataObject', 'one') + ); + } + + public function testGetIds() { + $factory = new FixtureFactory(); + $obj = $factory->createObject('FixtureFactoryTest_DataObject', 'one'); + $this->assertEquals( + array('one' => $obj->ID), + $factory->getIds('FixtureFactoryTest_DataObject') + ); + } + + public function testDefine() { + $factory = new FixtureFactory(); + $this->assertFalse($factory->getBlueprint('FixtureFactoryTest_DataObject')); + $factory->define('FixtureFactoryTest_DataObject'); + $this->assertInstanceOf( + 'FixtureBluePrint', + $factory->getBlueprint('FixtureFactoryTest_DataObject') + ); + } + + public function testDefineWithCustomBlueprint() { + $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); + $factory = new FixtureFactory(); + $this->assertFalse($factory->getBlueprint('FixtureFactoryTest_DataObject')); + $factory->define('FixtureFactoryTest_DataObject', $blueprint); + $this->assertInstanceOf( + 'FixtureBluePrint', + $factory->getBlueprint('FixtureFactoryTest_DataObject') + ); + $this->assertEquals( + $blueprint, + $factory->getBlueprint('FixtureFactoryTest_DataObject') + ); + } + + public function testDefineWithDefaults() { + $factory = new FixtureFactory(); + $factory->define('FixtureFactoryTest_DataObject', array('Name' => 'Default')); + $obj = $factory->createObject('FixtureFactoryTest_DataObject', 'one'); + $this->assertEquals('Default', $obj->Name); + } + + public function testDefineMultipleBlueprintsForClass() { + $factory = new FixtureFactory(); + $factory->define( + 'FixtureFactoryTest_DataObject', + new FixtureBlueprint('FixtureFactoryTest_DataObject') + ); + $factory->define( + 'FixtureFactoryTest_DataObjectWithDefaults', + new FixtureBlueprint( + 'FixtureFactoryTest_DataObjectWithDefaults', + 'FixtureFactoryTest_DataObject', + array('Name' => 'Default') + ) + ); + + $obj = $factory->createObject('FixtureFactoryTest_DataObject', 'one'); + $this->assertNull($obj->Name); + + $objWithDefaults = $factory->createObject('FixtureFactoryTest_DataObjectWithDefaults', 'two'); + $this->assertEquals('Default', $objWithDefaults->Name); + + $this->assertEquals( + $obj->ID, + $factory->getId('FixtureFactoryTest_DataObject', 'one') + ); + $this->assertEquals( + $objWithDefaults->ID, + $factory->getId('FixtureFactoryTest_DataObject', 'two'), + 'Can access fixtures under class name, not blueprint name' + ); + } + + public function testClear() { + $factory = new FixtureFactory(); + $obj1Id = $factory->createRaw('FixtureFactoryTest_DataObject', 'one', array('Name' => 'My Name')); + $obj2 = $factory->createObject('FixtureFactoryTest_DataObject', 'two'); + + $factory->clear(); + + $this->assertFalse($factory->getId('FixtureFactoryTest_DataObject', 'one')); + $this->assertNull(FixtureFactoryTest_DataObject::get()->byId($obj1Id)); + $this->assertFalse($factory->getId('FixtureFactoryTest_DataObject', 'two')); + $this->assertNull(FixtureFactoryTest_DataObject::get()->byId($obj2->ID)); + } + + public function testClearWithClass() { + $factory = new FixtureFactory(); + $obj1 = $factory->createObject('FixtureFactoryTest_DataObject', 'object-one'); + $relation1 = $factory->createObject('FixtureFactoryTest_DataObjectRelation', 'relation-one'); + + $factory->clear('FixtureFactoryTest_DataObject'); + + $this->assertFalse( + $factory->getId('FixtureFactoryTest_DataObject', 'one') + ); + $this->assertNull(FixtureFactoryTest_DataObject::get()->byId($obj1->ID)); + $this->assertEquals( + $relation1->ID, + $factory->getId('FixtureFactoryTest_DataObjectRelation', 'relation-one') + ); + $this->assertInstanceOf( + 'FixtureFactoryTest_DataObjectRelation', + FixtureFactoryTest_DataObjectRelation::get()->byId($relation1->ID) + ); + } + +} + +class FixtureFactoryTest_DataObject extends DataObject implements TestOnly { + static $db = array( + "Name" => "Varchar" + ); + static $many_many = array( + "ManyMany" => "FixtureFactoryTest_DataObjectRelation" + ); +} + +class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly { + static $db = array( + "Name" => "Varchar" + ); + static $belongs_many_many = array( + "TestParent" => "FixtureFactoryTest_DataObject" + ); +} diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index df4d59e0a..65f03abff 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -259,8 +259,8 @@ class DataObjectTest extends SapphireTest { $team->Comments()->add($newComment); $this->assertEquals($team->ID, $newComment->TeamID); - $comment1 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment1'); - $comment2 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment2'); + $comment1 = $this->objFromFixture('DataObjectTest_TeamComment', 'comment1'); + $comment2 = $this->objFromFixture('DataObjectTest_TeamComment', 'comment2'); $team->Comments()->remove($comment2); $commentIDs = $team->Comments()->sort('ID')->column('ID'); diff --git a/tests/testing/YamlFixtureTest.php b/tests/testing/YamlFixtureTest.php index ee4ea71b3..0a0c9bf6b 100644 --- a/tests/testing/YamlFixtureTest.php +++ b/tests/testing/YamlFixtureTest.php @@ -2,8 +2,6 @@ class YamlFixtureTest extends SapphireTest { - static $fixture_file = 'YamlFixtureTest.yml'; - protected $extraDataObjects = array( 'YamlFixtureTest_DataObject', 'YamlFixtureTest_DataObjectRelation', @@ -11,14 +9,14 @@ class YamlFixtureTest extends SapphireTest { public function testAbsoluteFixturePath() { $absPath = FRAMEWORK_PATH . '/tests/testing/YamlFixtureTest.yml'; - $obj = new YamlFixture($absPath); + $obj = Injector::inst()->create('YamlFixture', $absPath); $this->assertEquals($absPath, $obj->getFixtureFile()); $this->assertNull($obj->getFixtureString()); } public function testRelativeFixturePath() { $relPath = FRAMEWORK_DIR . '/tests/testing/YamlFixtureTest.yml'; - $obj = new YamlFixture($relPath); + $obj = Injector::inst()->create('YamlFixture', $relPath); $this->assertEquals(Director::baseFolder() . '/' . $relPath, $obj->getFixtureFile()); $this->assertNull($obj->getFixtureString()); } @@ -26,7 +24,7 @@ class YamlFixtureTest extends SapphireTest { public function testStringFixture() { $absPath = FRAMEWORK_PATH . '/tests/testing/YamlFixtureTest.yml'; $string = file_get_contents($absPath); - $obj = new YamlFixture($string); + $obj = Injector::inst()->create('YamlFixture', $string); $this->assertEquals($string, $obj->getFixtureString()); $this->assertNull($obj->getFixtureFile()); } @@ -36,16 +34,42 @@ class YamlFixtureTest extends SapphireTest { */ public function testFailsWithInvalidFixturePath() { $invalidPath = FRAMEWORK_DIR . '/tests/testing/invalid.yml'; - $obj = new YamlFixture($invalidPath); + $obj = Injector::inst()->create('YamlFixture', $invalidPath); } public function testSQLInsert() { - $object1 = DataObject::get_by_id("YamlFixtureTest_DataObject", - $this->idFromFixture("YamlFixtureTest_DataObject", "testobject1")); - $this->assertTrue($object1->ManyMany()->Count() == 2, "Should be 2 items in this manymany relationship"); - $object2 = DataObject::get_by_id("YamlFixtureTest_DataObject", - $this->idFromFixture("YamlFixtureTest_DataObject", "testobject2")); - $this->assertTrue($object2->ManyMany()->Count() == 2, "Should be 2 items in this manymany relationship"); + $relPath = FRAMEWORK_DIR . '/tests/testing/YamlFixtureTest.yml'; + $fixture = Injector::inst()->create('YamlFixture', $relPath); + $fixture->saveIntoDatabase(DataModel::inst()); + + $this->assertGreaterThan(0, $fixture->idFromFixture("YamlFixtureTest_DataObject", "testobject1")); + $object1 = DataObject::get_by_id( + "YamlFixtureTest_DataObject", + $fixture->idFromFixture("YamlFixtureTest_DataObject", "testobject1") + ); + $this->assertTrue( + $object1->ManyMany()->Count() == 2, + "Should be two items in this relationship" + ); + $this->assertGreaterThan(0, $fixture->idFromFixture("YamlFixtureTest_DataObject", "testobject2")); + $object2 = DataObject::get_by_id( + "YamlFixtureTest_DataObject", + $fixture->idFromFixture("YamlFixtureTest_DataObject", "testobject2") + ); + $this->assertTrue( + $object2->ManyMany()->Count() == 1, + "Should be one item in this relationship" + ); + } + + public function testWriteInto() { + $factory = Injector::inst()->create('FixtureFactory'); + + $relPath = FRAMEWORK_DIR . '/tests/testing/YamlFixtureTest.yml'; + $fixture = Injector::inst()->create('YamlFixture', $relPath); + $fixture->writeInto($factory); + + $this->assertGreaterThan(0, $factory->getId("YamlFixtureTest_DataObject", "testobject1")); } } diff --git a/tests/testing/YamlFixtureTest.yml b/tests/testing/YamlFixtureTest.yml index 766785691..b2a60f7b9 100644 --- a/tests/testing/YamlFixtureTest.yml +++ b/tests/testing/YamlFixtureTest.yml @@ -1,23 +1,12 @@ -YamlFixtureTest_DataObject: - testobject1: - Name: TestObject1 - testobject2: - Name: TestObject2 YamlFixtureTest_DataObjectRelation: relation1: Name: Relation1 relation2: Name: Relation2 -YamlFixtureTest_DataObject_ManyMany: - manymany1: - YamlFixtureTest_DataObjectID: =>YamlFixtureTest_DataObject.testobject1 - YamlFixtureTest_DataObjectRelationID: =>YamlFixtureTest_DataObjectRelation.relation1 - manymany2: - YamlFixtureTest_DataObjectID: =>YamlFixtureTest_DataObject.testobject1 - YamlFixtureTest_DataObjectRelationID: =>YamlFixtureTest_DataObjectRelation.relation2 - manymany3: - YamlFixtureTest_DataObjectID: =>YamlFixtureTest_DataObject.testobject2 - YamlFixtureTest_DataObjectRelationID: =>YamlFixtureTest_DataObjectRelation.relation1 - manymany4: - YamlFixtureTest_DataObjectID: =>YamlFixtureTest_DataObject.testobject2 - YamlFixtureTest_DataObjectRelationID: =>YamlFixtureTest_DataObjectRelation.relation2 \ No newline at end of file +YamlFixtureTest_DataObject: + testobject1: + Name: TestObject1 + ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1,=>YamlFixtureTest_DataObjectRelation.relation2 + testobject2: + Name: TestObject2 + ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1 \ No newline at end of file