Commit 87be9dea by Qiang Xue

refactored fixture.

parent 5ac1ec5d
...@@ -7,7 +7,7 @@ you to define your fixtures precisely and use them easily. ...@@ -7,7 +7,7 @@ you to define your fixtures precisely and use them easily.
A key concept in the Yii fixture framework is the so-called *fixture objects*. A fixture object is an instance A key concept in the Yii fixture framework is the so-called *fixture objects*. A fixture object is an instance
of [[yii\test\Fixture]] or its child class. It represents a particular aspect of a test environment. For example, of [[yii\test\Fixture]] or its child class. It represents a particular aspect of a test environment. For example,
you may define `UserFixture` to initialize the user database table with a set of known data. You load one or multiple you may define `UserFixture` to create the user table and populate it with some known data. You load one or multiple
fixture objects before running a test and unload them when finishing. fixture objects before running a test and unload them when finishing.
A fixture may depend on other fixtures, specified via its [[yii\test\Fixture::depends]] property. A fixture may depend on other fixtures, specified via its [[yii\test\Fixture::depends]] property.
...@@ -22,15 +22,16 @@ To define a fixture, create a new class by extending [[yii\test\Fixture]] or [[y ...@@ -22,15 +22,16 @@ To define a fixture, create a new class by extending [[yii\test\Fixture]] or [[y
The former is best suited for general purpose fixtures, while the latter has enhanced features specifically The former is best suited for general purpose fixtures, while the latter has enhanced features specifically
designed to work with database and ActiveRecord. designed to work with database and ActiveRecord.
If you extend from [[yii\test\Fixture]], make sure you override the [[yii\test\Fixture::load()]] method If you extend from [[yii\test\Fixture]], you should normally override the [[yii\test\Fixture::load()]] method
with your custom code of setting up the test environment (e.g. creating specific directories or files). with your custom code of setting up the test environment (e.g. creating specific directories or files).
In the following, we will mainly describe how to define a database fixture by extending [[yii\test\ActiveFixture]]. In the following, we will mainly describe how to define a database fixture by extending [[yii\test\ActiveFixture]].
Each `ActiveFixture` is about setting up the test data needed by a database table. You may specify the table Each `ActiveFixture` is about preparing a DB table for testing purpose. You may specify the table
by setting either the [[yii\test\ActiveFixture::tableName]] property or the [[yii\test\ActiveFixture::modelClass]] by setting either the [[yii\test\ActiveFixture::tableName]] property or the [[yii\test\ActiveFixture::modelClass]]
property. The latter takes the name of an `ActiveRecord` class whose associated table will be used by the fixture. property. If the latter, the table name will be taken from the `ActiveRecord` class specified by `modelClass`.
```php ```php
<?php
namespace app\tests\fixtures; namespace app\tests\fixtures;
use yii\test\ActiveFixture; use yii\test\ActiveFixture;
...@@ -41,26 +42,49 @@ class UserFixture extends ActiveFixture ...@@ -41,26 +42,49 @@ class UserFixture extends ActiveFixture
} }
``` ```
Next, you should provide the data needed by the user table in a file. By default, the file should be located at Next, you should override [[yii\test\ActiveFixture::loadSchema()]] to create the table. You may wonder why we need
`FixturePath/data/TableName.php`, where `FixturePath` stands for the directory containing the fixture class file, to create the table when loading a fixture and why we do not work with a database which already has the table. This
and `TableName` is the name of the table associated with the fixture. In the example above, the file should be is because preparing a complete test database is often very time consuming and in most test cases, only a very tiny part
`@app/tests/fixtures/data/tbl_user.php`, assuming the table associated with `User` is `tbl_user`. of the database is touched. So the idea here is to create the table only when it is needed by the test.
```php
<?php
namespace app\tests\fixtures;
The data file should return an array of data rows to be inserted into the user table. For example, use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
protected function loadSchema()
{
$this->createTable('tbl_user', [
'username' => 'string not null',
'email' => 'string not null',
...
]);
}
}
```
Lastly, you should provide the fixture data in a file located at `FixturePath/data/TableName.php`,
where `FixturePath` stands for the directory containing the fixture class file, and `TableName`
is the name of the table associated with the fixture. In the example above, the file should be
`@app/tests/fixtures/data/tbl_user.php`. The data file should return an array of data rows
to be inserted into the user table. For example,
```php ```php
<?php <?php
return [ return [
'user1' => [ 'user1' => [
'name' => 'Chase', 'username' => 'lmayert',
'login' => 'lmayert',
'email' => 'strosin.vernice@jerde.com', 'email' => 'strosin.vernice@jerde.com',
'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV', 'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2', 'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
], ],
'user2' => [ 'user2' => [
'name' => 'Celestine', 'username' => 'napoleon69',
'login' => 'napoleon69',
'email' => 'aileen.barton@heaneyschumm.com', 'email' => 'aileen.barton@heaneyschumm.com',
'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q', 'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6', 'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
...@@ -74,11 +98,11 @@ the two rows are aliased as `user1` and `user2`, respectively. ...@@ -74,11 +98,11 @@ the two rows are aliased as `user1` and `user2`, respectively.
Also, you do not need to specify the data for auto-incremental columns. Yii will automatically fill the actual Also, you do not need to specify the data for auto-incremental columns. Yii will automatically fill the actual
values into the rows when the fixture is being loaded. values into the rows when the fixture is being loaded.
> Info: You may customize the location of the data file by setting the [[yii\test\ActiveFixture::dataFile]] property. > Tip: You may customize the location of the data file by setting the [[yii\test\ActiveFixture::dataFile]] property.
> If you set this property to be false, or if you do not provide the data file, the fixture will not load any data > You may also override [[yii\test\ActiveFixture::getData()]] to provide the data.
> into the user table.
As we described earlier, a fixture may depend on other fixtures. For example, `UserProfileFixture` depends on `UserFixture`. As we described earlier, a fixture may depend on other fixtures. For example, `UserProfileFixture` depends on `UserFixture`
because the user profile table contains a foreign key pointing to the user table.
The dependency is specified via the [[yii\test\Fixture::depends]] property, like the following, The dependency is specified via the [[yii\test\Fixture::depends]] property, like the following,
```php ```php
...@@ -89,7 +113,7 @@ use yii\test\ActiveFixture; ...@@ -89,7 +113,7 @@ use yii\test\ActiveFixture;
class UserProfileFixture extends ActiveFixture class UserProfileFixture extends ActiveFixture
{ {
public $modelClass = 'app\models\UserProfile'; public $modelClass = 'app\models\UserProfile';
public $depends = ['yii\test\DbFixture', 'app\tests\fixtures\UserFixture']; public $depends = ['app\tests\fixtures\UserFixture'];
} }
``` ```
...@@ -108,16 +132,17 @@ fixtures. More often you would develop your test cases by using the `yii2-codece ...@@ -108,16 +132,17 @@ fixtures. More often you would develop your test cases by using the `yii2-codece
which uses [[yii\test\FixtureTrait]] and has the built-in support for the loading and accessing fixtures. which uses [[yii\test\FixtureTrait]] and has the built-in support for the loading and accessing fixtures.
In the following we will describe how to write a `UserProfile` unit test class using `yii2-codeception`. In the following we will describe how to write a `UserProfile` unit test class using `yii2-codeception`.
In your unit test class extending [[yii\codeception\TestCase]], declare which fixtures you want to use In your unit test class extending [[yii\codeception\DbTestCase]] (or [[yii\codeception\TestCase]] if you are NOT
in the [[yii\testFixtureTrait::fixtures()|fixtures()]] method. For example, testing DB-related features), declare which fixtures you want to use in the [[yii\testFixtureTrait::fixtures()|fixtures()]] method.
For example,
```php ```php
namespace app\tests\unit\models; namespace app\tests\unit\models;
use yii\codeception\TestCase; use yii\codeception\DbTestCase;
use app\tests\fixtures\UserProfileFixture; use app\tests\fixtures\UserProfileFixture;
class UserProfileTest extends TestCase class UserProfileTest extends DbTestCase
{ {
protected function fixtures() protected function fixtures()
{ {
...@@ -133,8 +158,8 @@ class UserProfileTest extends TestCase ...@@ -133,8 +158,8 @@ class UserProfileTest extends TestCase
The fixtures listed in the `fixtures()` method will be automatically loaded before running every test method The fixtures listed in the `fixtures()` method will be automatically loaded before running every test method
in the test case and unloaded after finishing every test method. And as we described before, when a fixture is in the test case and unloaded after finishing every test method. And as we described before, when a fixture is
being loaded, all its dependent fixtures will be automatically loaded first. In the above example, because being loaded, all its dependent fixtures will be automatically loaded first. In the above example, because
`UserProfileFixture` depends on `UserFixture` and `DbFixture`, when running any test method in the test class, `UserProfileFixture` depends on `UserFixture`, when running any test method in the test class,
three fixtures will be loaded sequentially: `DbFixture`, `UserFixture` and `UserProfileFixture`. two fixtures will be loaded sequentially: `UserFixture` and `UserProfileFixture`.
When specifying fixtures in `fixtures()`, you may use either a class name or a configuration array to refer to When specifying fixtures in `fixtures()`, you may use either a class name or a configuration array to refer to
a fixture. The configuration array will let you customize the fixture properties when the fixture is loaded. a fixture. The configuration array will let you customize the fixture properties when the fixture is loaded.
...@@ -157,3 +182,19 @@ foreach ($this->profiles as $row) ... ...@@ -157,3 +182,19 @@ foreach ($this->profiles as $row) ...
> Info: `$this->profiles` is still of `UserProfileFixture` type. The above access features are implemented > Info: `$this->profiles` is still of `UserProfileFixture` type. The above access features are implemented
> through PHP magic methods. > through PHP magic methods.
Defining and Using Global Fixtures
----------------------------------
The fixtures described above are mainly used by individual test cases. In most cases, you also need some global
fixtures that are applied to ALL or many test cases. An example is [[yii\test\InitDbFixture]] which is used to
set up a skeleton test database and toggle database integrity checks when applying other DB fixtures.
This fixture will try to execute a script located at `@app/tests/fixtures/initdb.php`. In this script, you may,
for example, load a basic DB dump containing the minimal set of tables, etc.
Using global fixtures is similar to using non-global ones. The only difference is that you declare these fixtures
in [[yii\codeception\TestCase::globalFixtures()]] instead of `fixtures()`. When a test case load fixtures, it will
first load global fixtures and then non-global ones.
By default, [[yii\codeception\DbTestCase]] already declares `InitDbFixture` in its `globalFixtures()` method.
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\codeception;
use yii\test\InitDbFixture;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class DbTestCase extends TestCase
{
/**
* @inheritdoc
*/
protected function globalFixtures()
{
return [
InitDbFixture::className(),
];
}
}
...@@ -5,9 +5,6 @@ namespace yii\codeception; ...@@ -5,9 +5,6 @@ namespace yii\codeception;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use Codeception\TestCase\Test; use Codeception\TestCase\Test;
use yii\base\UnknownMethodException;
use yii\base\UnknownPropertyException;
use yii\test\ActiveFixture;
use yii\test\FixtureTrait; use yii\test\FixtureTrait;
/** /**
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
/**
* ArrayAccessTrait provides the implementation for `IteratorAggregate`, `ArrayAccess` and `Countable`.
*
* Note that ArrayAccessTrait requires the class using it contain a property named `data` which should be an array.
* The data will be exposed by ArrayAccessTrait to support accessing the class object like an array.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
trait ArrayAccessTrait
{
/**
* Returns an iterator for traversing the data.
* This method is required by the SPL interface `IteratorAggregate`.
* It will be implicitly called when you use `foreach` to traverse the collection.
* @return \ArrayIterator an iterator for traversing the cookies in the collection.
*/
public function getIterator()
{
return new \ArrayIterator($this->data);
}
/**
* Returns the number of data items.
* This method is required by Countable interface.
* @return integer number of data elements.
*/
public function count()
{
return count($this->data);
}
/**
* This method is required by the interface ArrayAccess.
* @param mixed $offset the offset to check on
* @return boolean
*/
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
/**
* This method is required by the interface ArrayAccess.
* @param integer $offset the offset to retrieve element.
* @return mixed the element at the offset, null if no element is found at the offset
*/
public function offsetGet($offset)
{
return isset($this->data[$offset]) ? $this->data[$offset] : null;
}
/**
* This method is required by the interface ArrayAccess.
* @param integer $offset the offset to set element
* @param mixed $item the element value
*/
public function offsetSet($offset, $item)
{
$this->data[$offset] = $item;
}
/**
* This method is required by the interface ArrayAccess.
* @param mixed $offset the offset to unset element
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}
...@@ -343,7 +343,6 @@ class Migration extends \yii\base\Component ...@@ -343,7 +343,6 @@ class Migration extends \yii\base\Component
* Builds and executes a SQL statement for dropping a primary key. * Builds and executes a SQL statement for dropping a primary key.
* @param string $name the name of the primary key constraint to be removed. * @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from. * @param string $table the table that the primary key constraint will be removed from.
* @return Command the command object itself
*/ */
public function dropPrimaryKey($name, $table) public function dropPrimaryKey($name, $table)
{ {
......
...@@ -8,64 +8,55 @@ ...@@ -8,64 +8,55 @@
namespace yii\test; namespace yii\test;
use Yii; use Yii;
use yii\base\ArrayAccessTrait;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\db\Connection;
use yii\db\TableSchema; use yii\db\TableSchema;
/** /**
* ActiveFixture represents a fixture backed up by a [[modelClass|ActiveRecord class]] or a [[tableName|database table]]. * ActiveFixture represents a fixture backed up by a [[modelClass|ActiveRecord class]] or a [[tableName|database table]].
* *
* Either [[modelClass]] or [[tableName]] must be set. When loading an ActiveFixture, the corresponding * Either [[modelClass]] or [[tableName]] must be set. And you should normally override [[loadSchema()]]
* database table will be [[resetTable()|reset]] first. It will then be populated with the data loaded by [[loadData()]]. * to set up the necessary database schema (e.g. creating the table, view, trigger, etc.)
* You should also provide fixture data in the file specified by [[dataFile]] or overriding [[loadData()]] if you want
* to use code to generate the fixture data.
* *
* You can access the loaded data via the [[rows]] property. If you set [[modelClass]], you will also be able * When the fixture is being loaded, it will first call [[loadSchema()]] to initialize the database schema.
* to retrieve an instance of [[modelClass]] with the populated data via [[getModel()]]. * It will then call [[loadData()]] to populate the table with the fixture data.
*
* After the fixture is loaded, you can access the loaded data via the [[data]] property. If you set [[modelClass]],
* you will also be able to retrieve an instance of [[modelClass]] with the populated data via [[getModel()]].
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, \Countable class ActiveFixture extends BaseActiveFixture
{ {
/** /**
* @inheritdoc
*/
public $depends = ['yii\test\DbFixture'];
/**
* @var Connection|string the DB connection object or the application component ID of the DB connection.
* After the ActiveFixture object is created, if you want to change this property, you should only assign it
* with a DB connection object.
*/
public $db = 'db';
/**
* @var string the AR model class associated with this fixture.
* @see tableName
*/
public $modelClass;
/**
* @var string the name of the database table that this fixture is about. If this property is not set, * @var string the name of the database table that this fixture is about. If this property is not set,
* the table name will be determined via [[modelClass]]. * the table name will be determined via [[modelClass]].
* @see modelClass * @see modelClass
*/ */
public $tableName; public $tableName;
/** /**
* @var string|boolean the file path or path alias of the data file that contains the fixture data * @var string the file path or path alias of the data file that contains the fixture data
* and will be loaded by [[loadData()]]. If this is not set, it will default to `FixturePath/data/TableName.php`, * and will be loaded by [[loadData()]]. If this is not set, it will default to `FixturePath/data/TableName.php`,
* where `FixturePath` stands for the directory containing this fixture class, and `TableName` stands for the * where `FixturePath` stands for the directory containing this fixture class, and `TableName` stands for the
* name of the table associated with this fixture. You may set this property to be false to disable loading data. * name of the table associated with this fixture.
*/ */
public $dataFile; public $dataFile;
/** /**
* @var array the data rows. Each array element represents one row of data (column name => column value). * @var boolean whether to reset the table associated with this fixture.
* By setting this property to be true, when [[loadData()]] is called, all existing data in the table
* will be removed and the sequence number (if any) will be reset.
*
* Note that you normally do not need to reset the table if you implement [[loadSchema()]] because
* there will be no existing data.
*/ */
public $rows; public $resetTable = false;
/** /**
* @var TableSchema the table schema for the table associated with this fixture * @var TableSchema the table schema for the table associated with this fixture
*/ */
private $_table; private $_table;
/**
* @var \yii\db\ActiveRecord[] the loaded AR models
*/
private $_models;
/** /**
* @inheritdoc * @inheritdoc
...@@ -76,68 +67,6 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, ...@@ -76,68 +67,6 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess,
if (!isset($this->modelClass) && !isset($this->tableName)) { if (!isset($this->modelClass) && !isset($this->tableName)) {
throw new InvalidConfigException('Either "modelClass" or "tableName" must be set.'); throw new InvalidConfigException('Either "modelClass" or "tableName" must be set.');
} }
if (is_string($this->db)) {
$this->db = Yii::$app->getComponent($this->db);
}
if (!$this->db instanceof Connection) {
throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection.");
}
}
/**
* @inheritdoc
*/
public function load()
{
$this->initSchema();
$table = $this->getTableSchema();
$this->resetTable();
$this->rows = [];
$this->_models = [];
foreach ($this->loadData() as $alias => $row) {
$this->db->createCommand()->insert($table->fullName, $row)->execute();
if ($table->sequenceName !== null) {
foreach ($table->primaryKey as $pk) {
if (!isset($row[$pk])) {
$row[$pk] = $this->db->getLastInsertID($table->sequenceName);
break;
}
}
}
$this->rows[$alias] = $row;
}
}
/**
* Returns the AR model by the specified model name.
* A model name is the key of the corresponding data row returned by [[loadData()]].
* @param string $name the model name.
* @return null|\yii\db\ActiveRecord the AR model, or null if the model cannot be found in the database
* @throws \yii\base\InvalidConfigException if [[modelClass]] is not set.
*/
public function getModel($name)
{
if (!isset($this->rows[$name])) {
return null;
}
if (array_key_exists($name, $this->_models)) {
return $this->_models[$name];
}
if ($this->modelClass === null) {
throw new InvalidConfigException('The "modelClass" property must be set.');
}
$row = $this->rows[$name];
/** @var \yii\db\ActiveRecord $modelClass */
$modelClass = $this->modelClass;
/** @var \yii\db\ActiveRecord $model */
$model = new $modelClass;
$keys = [];
foreach ($model->primaryKey() as $key) {
$keys[$key] = isset($row[$key]) ? $row[$key] : null;
}
return $this->_models[$name] = $modelClass::find($keys);
} }
/** /**
...@@ -167,25 +96,43 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, ...@@ -167,25 +96,43 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess,
} }
/** /**
* Initializes the database schema. * Loads the fixture data.
* This method is called by [[load()]] before loading data. * The default implementation will first reset the DB table and then populate it with the data
* You may override this method to prepare necessary database schema changes. * returned by [[getData()]].
*/ */
protected function initSchema() protected function loadData()
{ {
$table = $this->getTableSchema();
if ($this->resetTable) {
$this->resetTable();
}
foreach ($this->getData() as $alias => $row) {
$this->db->createCommand()->insert($table->fullName, $row)->execute();
if ($table->sequenceName !== null) {
foreach ($table->primaryKey as $pk) {
if (!isset($row[$pk])) {
$row[$pk] = $this->db->getLastInsertID($table->sequenceName);
break;
}
}
}
$this->data[$alias] = $row;
}
} }
/** /**
* Loads fixture data. * Returns the fixture data.
*
* This method is called by [[loadData()]] to get the needed fixture data.
* *
* The default implementation will try to load data by including the external file specified by [[dataFile]]. * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
* The file should return an array of data rows (column name => column value), each corresponding to a row in the table. * The file should return an array of data rows (column name => column value), each corresponding to a row in the table.
* *
* If the data file does not exist, an empty array will be returned. * If the data file does not exist, an empty array will be returned.
* *
* @return array the data rows to be inserted into the database table. * @return array the data rows to be inserted into the database table.
*/ */
protected function loadData() protected function getData()
{ {
if ($this->dataFile === false) { if ($this->dataFile === false) {
return []; return [];
...@@ -211,64 +158,4 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, ...@@ -211,64 +158,4 @@ class ActiveFixture extends Fixture implements \IteratorAggregate, \ArrayAccess,
$this->db->createCommand()->resetSequence($table->fullName, 1)->execute(); $this->db->createCommand()->resetSequence($table->fullName, 1)->execute();
} }
} }
/**
* Returns an iterator for traversing the cookies in the collection.
* This method is required by the SPL interface `IteratorAggregate`.
* It will be implicitly called when you use `foreach` to traverse the collection.
* @return \ArrayIterator an iterator for traversing the cookies in the collection.
*/
public function getIterator()
{
return new \ArrayIterator($this->rows);
}
/**
* Returns the number of items in the session.
* This method is required by Countable interface.
* @return integer number of items in the session.
*/
public function count()
{
return count($this->rows);
}
/**
* This method is required by the interface ArrayAccess.
* @param mixed $offset the offset to check on
* @return boolean
*/
public function offsetExists($offset)
{
return isset($this->rows[$offset]);
}
/**
* This method is required by the interface ArrayAccess.
* @param integer $offset the offset to retrieve element.
* @return mixed the element at the offset, null if no element is found at the offset
*/
public function offsetGet($offset)
{
return isset($this->rows[$offset]) ? $this->rows[$offset] : null;
}
/**
* This method is required by the interface ArrayAccess.
* @param integer $offset the offset to set element
* @param mixed $item the element value
*/
public function offsetSet($offset, $item)
{
$this->rows[$offset] = $item;
}
/**
* This method is required by the interface ArrayAccess.
* @param mixed $offset the offset to unset element
*/
public function offsetUnset($offset)
{
unset($this->rows[$offset]);
}
} }
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\test;
use Yii;
use yii\base\ArrayAccessTrait;
use yii\base\InvalidConfigException;
/**
* BaseActiveFixture is the base class for fixture classes that support accessing fixture data as ActiveRecord objects.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate, \ArrayAccess, \Countable
{
use ArrayAccessTrait;
/**
* @var string the AR model class associated with this fixture.
* @see tableName
*/
public $modelClass;
/**
* @var boolean whether to create the corresponding DB schema for this fixture.
* By setting this property to be true, the [[loadSchema()]] method will be called when the fixture is loaded.
*/
public $loadSchema = true;
/**
* @var boolean whether to load fixture data.
* By setting this property to be true, the [[loadData()]] method will be called when the fixture is loaded.
*/
public $loadData = true;
/**
* @var array the data rows. Each array element represents one row of data (column name => column value).
*/
public $data = [];
/**
* @var \yii\db\ActiveRecord[] the loaded AR models
*/
private $_models = [];
/**
* @inheritdoc
*/
public function load()
{
if ($this->loadSchema) {
$this->loadSchema();
}
if ($this->loadData) {
$this->loadData();
}
}
/**
* Returns the AR model by the specified model name.
* A model name is the key of the corresponding data row returned by [[loadData()]].
* @param string $name the model name.
* @return null|\yii\db\ActiveRecord the AR model, or null if the model cannot be found in the database
* @throws \yii\base\InvalidConfigException if [[modelClass]] is not set.
*/
public function getModel($name)
{
if (!isset($this->data[$name])) {
return null;
}
if (array_key_exists($name, $this->_models)) {
return $this->_models[$name];
}
if ($this->modelClass === null) {
throw new InvalidConfigException('The "modelClass" property must be set.');
}
$row = $this->data[$name];
/** @var \yii\db\ActiveRecord $modelClass */
$modelClass = $this->modelClass;
/** @var \yii\db\ActiveRecord $model */
$model = new $modelClass;
$keys = [];
foreach ($model->primaryKey() as $key) {
$keys[$key] = isset($row[$key]) ? $row[$key] : null;
}
return $this->_models[$name] = $modelClass::find($keys);
}
/**
* Creates the database schema needed by this fixture.
* You may override this method by creating the DB table associated with this fixture
* and other relevant DB elements, such as views, triggers.
*/
protected function loadSchema()
{
}
/**
* Loads the fixture data.
* The default implementation will first reset the DB table and then populate it with the data
* returned by [[getData()]].
*/
protected function loadData()
{
return [];
}
}
...@@ -12,18 +12,14 @@ use yii\base\InvalidConfigException; ...@@ -12,18 +12,14 @@ use yii\base\InvalidConfigException;
use yii\db\Connection; use yii\db\Connection;
/** /**
* DbFixture represents the fixture needed for setting up a DB connection. * DbFixture is the base class for DB-related fixtures.
* *
* Its main task is to toggle integrity check of the database during data loading. * DbFixture provides the [[db]] connection as well as a set of commonly used DB manipulation methods.
* This is needed by other DB-related fixtures (e.g. [[ActiveFixture]]) so that they can populate
* data into the database without triggering integrity check errors.
*
* Besides, DbFixture also attempts to load an [[initScript|initialization script]] if it exists.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class DbFixture extends Fixture abstract class DbFixture extends Fixture
{ {
/** /**
* @var Connection|string the DB connection object or the application component ID of the DB connection. * @var Connection|string the DB connection object or the application component ID of the DB connection.
...@@ -31,19 +27,6 @@ class DbFixture extends Fixture ...@@ -31,19 +27,6 @@ class DbFixture extends Fixture
* with a DB connection object. * with a DB connection object.
*/ */
public $db = 'db'; public $db = 'db';
/**
* @var string the init script file that should be executed when loading this fixture.
* This should be either a file path or path alias. Note that if the file does not exist,
* no error will be raised.
*/
public $initScript = '@app/tests/fixtures/init.php';
/**
* @var array list of database schemas that the test tables may reside in. Defaults to
* [''], meaning using the default schema (an empty string refers to the
* default schema). This property is mainly used when turning on and off integrity checks
* so that fixture data can be populated into the database without causing problem.
*/
public $schemas = [''];
/** /**
* @inheritdoc * @inheritdoc
...@@ -54,47 +37,260 @@ class DbFixture extends Fixture ...@@ -54,47 +37,260 @@ class DbFixture extends Fixture
if (is_string($this->db)) { if (is_string($this->db)) {
$this->db = Yii::$app->getComponent($this->db); $this->db = Yii::$app->getComponent($this->db);
} }
if (!$this->db instanceof Connection) { if (!is_object($this->db)) {
throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection."); throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection.");
} }
} }
/** /**
* @inheritdoc * Executes a SQL statement.
* This method executes the specified SQL statement using [[db]].
* @param string $sql the SQL statement to be executed
* @param array $params input parameters (name => value) for the SQL execution.
* See [[Command::execute()]] for more details.
*/ */
public function beforeLoad() public function execute($sql, $params = [])
{ {
$this->checkIntegrity(false); $this->db->createCommand($sql)->execute($params);
} }
/** /**
* @inheritdoc * Creates and executes an INSERT SQL statement.
* The method will properly escape the column names, and bind the values to be inserted.
* @param string $table the table that new rows will be inserted into.
* @param array $columns the column data (name => value) to be inserted into the table.
*/ */
public function afterLoad() public function insert($table, $columns)
{ {
$this->checkIntegrity(true); $this->db->createCommand()->insert($table, $columns)->execute();
} }
/** /**
* @inheritdoc * Creates and executes an batch INSERT SQL statement.
* The method will properly escape the column names, and bind the values to be inserted.
* @param string $table the table that new rows will be inserted into.
* @param array $columns the column names.
* @param array $rows the rows to be batch inserted into the table
*/ */
public function load() public function batchInsert($table, $columns, $rows)
{ {
$file = Yii::getAlias($this->initScript); $this->db->createCommand()->batchInsert($table, $columns, $rows)->execute();
if (is_file($file)) {
require($file);
}
} }
/** /**
* Enables or disables database integrity check. * Creates and executes an UPDATE SQL statement.
* This method may be used to temporarily turn off foreign constraints check. * The method will properly escape the column names and bind the values to be updated.
* @param boolean $check whether to enable database integrity check * @param string $table the table to be updated.
* @param array $columns the column data (name => value) to be updated.
* @param array|string $condition the conditions that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify conditions.
* @param array $params the parameters to be bound to the query.
*/ */
public function checkIntegrity($check) public function update($table, $columns, $condition = '', $params = [])
{ {
foreach ($this->schemas as $schema) { $this->db->createCommand()->update($table, $columns, $condition, $params)->execute();
$this->db->createCommand()->checkIntegrity($check, $schema)->execute(); }
}
/**
* Creates and executes a DELETE SQL statement.
* @param string $table the table where the data will be deleted from.
* @param array|string $condition the conditions that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify conditions.
* @param array $params the parameters to be bound to the query.
*/
public function delete($table, $condition = '', $params = [])
{
$this->db->createCommand()->delete($table, $condition, $params)->execute();
}
/**
* Builds and executes a SQL statement for creating a new DB table.
*
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* where name stands for a column name which will be properly quoted by the method, and definition
* stands for the column type which can contain an abstract DB type.
*
* The [[QueryBuilder::getColumnType()]] method will be invoked to convert any abstract type into a physical one.
*
* If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
* put into the generated SQL.
*
* @param string $table the name of the table to be created. The name will be properly quoted by the method.
* @param array $columns the columns (name => definition) in the new table.
* @param string $options additional SQL fragment that will be appended to the generated SQL.
*/
public function createTable($table, $columns, $options = null)
{
$this->db->createCommand()->createTable($table, $columns, $options)->execute();
}
/**
* Builds and executes a SQL statement for renaming a DB table.
* @param string $table the table to be renamed. The name will be properly quoted by the method.
* @param string $newName the new table name. The name will be properly quoted by the method.
*/
public function renameTable($table, $newName)
{
$this->db->createCommand()->renameTable($table, $newName)->execute();
}
/**
* Builds and executes a SQL statement for dropping a DB table.
* @param string $table the table to be dropped. The name will be properly quoted by the method.
*/
public function dropTable($table)
{
$this->db->createCommand()->dropTable($table)->execute();
}
/**
* Builds and executes a SQL statement for truncating a DB table.
* @param string $table the table to be truncated. The name will be properly quoted by the method.
*/
public function truncateTable($table)
{
$this->db->createCommand()->truncateTable($table)->execute();
}
/**
* Builds and executes a SQL statement for adding a new DB column.
* @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
* @param string $column the name of the new column. The name will be properly quoted by the method.
* @param string $type the column type. The [[QueryBuilder::getColumnType()]] method will be invoked to convert abstract column type (if any)
* into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
* For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
*/
public function addColumn($table, $column, $type)
{
$this->db->createCommand()->addColumn($table, $column, $type)->execute();
}
/**
* Builds and executes a SQL statement for dropping a DB column.
* @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
* @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
*/
public function dropColumn($table, $column)
{
$this->db->createCommand()->dropColumn($table, $column)->execute();
}
/**
* Builds and executes a SQL statement for renaming a column.
* @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
* @param string $name the old name of the column. The name will be properly quoted by the method.
* @param string $newName the new name of the column. The name will be properly quoted by the method.
*/
public function renameColumn($table, $name, $newName)
{
$this->db->createCommand()->renameColumn($table, $name, $newName)->execute();
}
/**
* Builds and executes a SQL statement for changing the definition of a column.
* @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
* @param string $column the name of the column to be changed. The name will be properly quoted by the method.
* @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
* into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
* For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
*/
public function alterColumn($table, $column, $type)
{
$this->db->createCommand()->alterColumn($table, $column, $type)->execute();
}
/**
* Builds and executes a SQL statement for creating a primary key.
* The method will properly quote the table and column names.
* @param string $name the name of the primary key constraint.
* @param string $table the table that the primary key constraint will be added to.
* @param string|array $columns comma separated string or array of columns that the primary key will consist of.
*/
public function addPrimaryKey($name, $table, $columns)
{
$this->db->createCommand()->addPrimaryKey($name, $table, $columns)->execute();
}
/**
* Builds and executes a SQL statement for dropping a primary key.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
*/
public function dropPrimaryKey($name, $table)
{
$this->db->createCommand()->dropPrimaryKey($name, $table)->execute();
}
/**
* Builds a SQL statement for adding a foreign key constraint to an existing table.
* The method will properly quote the table and column names.
* @param string $name the name of the foreign key constraint.
* @param string $table the table that the foreign key constraint will be added to.
* @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas.
* @param string $refTable the table that the foreign key references to.
* @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas.
* @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
* @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
*/
public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
{
$this->db->createCommand()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update)->execute();
}
/**
* Builds a SQL statement for dropping a foreign key constraint.
* @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
* @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
*/
public function dropForeignKey($name, $table)
{
$this->db->createCommand()->dropForeignKey($name, $table)->execute();
}
/**
* Builds and executes a SQL statement for creating a new index.
* @param string $name the name of the index. The name will be properly quoted by the method.
* @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
* @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them
* by commas. The column names will be properly quoted by the method.
* @param boolean $unique whether to add UNIQUE constraint on the created index.
*/
public function createIndex($name, $table, $column, $unique = false)
{
$this->db->createCommand()->createIndex($name, $table, $column, $unique)->execute();
}
/**
* Builds and executes a SQL statement for dropping an index.
* @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
* @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
*/
public function dropIndex($name, $table)
{
$this->db->createCommand()->dropIndex($name, $table)->execute();
}
/**
* Creates and executes a SQL command to reset the sequence value of a table's primary key.
* The sequence will be reset such that the primary key of the next new row inserted
* will have the specified value or 1.
* @param string $table the name of the table whose primary key sequence will be reset
* @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
* the next new row's primary key will have a value 1.
*/
public function resetSequence($table, $value = null)
{
$this->db->createCommand()->resetSequence($table, $value)->execute();
}
/**
* Builds and executes a SQL command for enabling or disabling integrity check.
* @param boolean $check whether to turn on or off the integrity check.
* @param string $schema the schema name of the tables. Defaults to empty string, meaning the current
* or default schema.
* @param string $table the table name.
*/
public function checkIntegrity($check = true, $schema = '', $table = '')
{
$this->db->createCommand()->checkIntegrity($check, $schema, $table)->execute();
} }
} }
...@@ -95,6 +95,9 @@ trait FixtureTrait ...@@ -95,6 +95,9 @@ trait FixtureTrait
* ] * ]
* ``` * ```
* *
* Note that the actual fixtures used for a test case will include both [[globalFixtures()]]
* and [[fixtures()]].
*
* @return array the fixtures needed by the current test case * @return array the fixtures needed by the current test case
*/ */
protected function fixtures() protected function fixtures()
...@@ -103,8 +106,20 @@ trait FixtureTrait ...@@ -103,8 +106,20 @@ trait FixtureTrait
} }
/** /**
* Declares the fixtures shared required by different test cases.
* The return value should be similar to that of [[fixtures()]].
* You should usually override this method in a base class.
* @return array the fixtures shared and required by different test cases.
* @see fixtures()
*/
protected function globalFixtures()
{
return [];
}
/**
* Loads the fixtures. * Loads the fixtures.
* This method will load the fixtures specified by `$fixtures` or [[fixtures()]]. * This method will load the fixtures specified by `$fixtures` or [[globalFixtures()]] and [[fixtures()]].
* @param array $fixtures the fixtures to loaded. If not set, [[fixtures()]] will be loaded instead. * @param array $fixtures the fixtures to loaded. If not set, [[fixtures()]] will be loaded instead.
* @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among
* the fixtures is detected. * the fixtures is detected.
...@@ -112,7 +127,7 @@ trait FixtureTrait ...@@ -112,7 +127,7 @@ trait FixtureTrait
protected function loadFixtures($fixtures = null) protected function loadFixtures($fixtures = null)
{ {
if ($fixtures === null) { if ($fixtures === null) {
$fixtures = $this->fixtures(); $fixtures = array_merge($this->globalFixtures(), $this->fixtures());
} }
// normalize fixture configurations // normalize fixture configurations
...@@ -130,7 +145,7 @@ trait FixtureTrait ...@@ -130,7 +145,7 @@ trait FixtureTrait
// create fixture instances // create fixture instances
$this->_fixtures = []; $this->_fixtures = [];
$stack = $fixtures; $stack = array_reverse($fixtures);
while (($fixture = array_pop($stack)) !== null) { while (($fixture = array_pop($stack)) !== null) {
if ($fixture instanceof Fixture) { if ($fixture instanceof Fixture) {
$class = get_class($fixture); $class = get_class($fixture);
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\test;
use Yii;
/**
* InitDbFixture represents the initial state needed for DB-related tests.
*
* Its main task is to toggle integrity check of the database during data loading.
* This is needed by other DB-related fixtures (e.g. [[ActiveFixture]]) so that they can populate
* data into the database without triggering integrity check errors.
*
* Besides, DbFixture also attempts to load an [[initScript|initialization script]] if it exists.
*
* You should normally use InitDbFixture to prepare a skeleton test database.
* Other DB fixtures will then add specific tables and data to this database.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class InitDbFixture extends DbFixture
{
/**
* @var string the init script file that should be executed when loading this fixture.
* This should be either a file path or path alias. Note that if the file does not exist,
* no error will be raised.
*/
public $initScript = '@app/tests/fixtures/initdb.php';
/**
* @var array list of database schemas that the test tables may reside in. Defaults to
* [''], meaning using the default schema (an empty string refers to the
* default schema). This property is mainly used when turning on and off integrity checks
* so that fixture data can be populated into the database without causing problem.
*/
public $schemas = [''];
/**
* @inheritdoc
*/
public function beforeLoad()
{
foreach ($this->schemas as $schema) {
$this->checkIntegrity(false, $schema);
}
}
/**
* @inheritdoc
*/
public function afterLoad()
{
foreach ($this->schemas as $schema) {
$this->checkIntegrity(true, $schema);
}
}
/**
* @inheritdoc
*/
public function load()
{
$file = Yii::getAlias($this->initScript);
if (is_file($file)) {
require($file);
}
}
}
...@@ -9,12 +9,36 @@ namespace yiiunit\framework\test; ...@@ -9,12 +9,36 @@ namespace yiiunit\framework\test;
use yii\test\ActiveFixture; use yii\test\ActiveFixture;
use yii\test\FixtureTrait; use yii\test\FixtureTrait;
use yiiunit\data\ar\Customer; use yii\test\InitDbFixture;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\framework\db\DatabaseTestCase; use yiiunit\framework\db\DatabaseTestCase;
class Customer extends ActiveRecord
{
public static function tableName()
{
return 'tbl_customer2';
}
}
class CustomerFixture extends ActiveFixture class CustomerFixture extends ActiveFixture
{ {
public $modelClass = 'yiiunit\data\ar\Customer'; public $modelClass = 'yiiunit\framework\test\Customer';
protected function loadSchema()
{
try {
$this->dropTable('tbl_customer2');
} catch (\Exception $e) {
}
$this->createTable('tbl_customer2', [
'id' => 'pk',
'email' => 'string',
'name' => 'string',
'address' => 'string',
'status' => 'integer',
]);
}
} }
class MyDbTestCase class MyDbTestCase
...@@ -37,6 +61,13 @@ class MyDbTestCase ...@@ -37,6 +61,13 @@ class MyDbTestCase
'customers' => CustomerFixture::className(), 'customers' => CustomerFixture::className(),
]; ];
} }
protected function globalFixtures()
{
return [
InitDbFixture::className(),
];
}
} }
/** /**
...@@ -58,7 +89,7 @@ class ActiveFixtureTest extends DatabaseTestCase ...@@ -58,7 +89,7 @@ class ActiveFixtureTest extends DatabaseTestCase
parent::tearDown(); parent::tearDown();
} }
public function testGetRows() public function testGetData()
{ {
$test = new MyDbTestCase(); $test = new MyDbTestCase();
$test->setUp(); $test->setUp();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment