Commit 3e9a0743 by Qiang Xue

Merge pull request #2050 from yiisoft/feature-fixture

Feature fixture
parents 641e1673 f77baa3e
......@@ -37,7 +37,7 @@ return [
];
```
This data will be loaded to the `users`, but before it will be loaded table `users` will be cleared: all data deleted, sequence reseted.
This data will be loaded to the `users`, but before it will be loaded table `users` will be cleared: all data deleted, sequence reset.
Above fixture example was auto-generated by `yii2-faker` extension, read more about it in these [section](#auto-generating-fixtures).
Applying fixtures
......
Fixtures
========
Fixtures are important part of testing. Their main purpose is to set up the environment in a fixed/known state
so that your tests are repeatable and run in an expected way. Yii provides a fixture framework that allows
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
of [[yii\test\Fixture]] or its child class. It represents a particular aspect of a test environment. For example,
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.
A fixture may depend on other fixtures, specified via its [[yii\test\Fixture::depends]] property.
When a fixture is being loaded, the fixtures it depends on will be automatically loaded BEFORE the fixture;
and when the fixture is being unloaded, the dependent fixtures will be unloaded AFTER the fixture.
Defining a Fixture
------------------
To define a fixture, create a new class by extending [[yii\test\Fixture]] or [[yii\test\ActiveFixture]].
The former is best suited for general purpose fixtures, while the latter has enhanced features specifically
designed to work with database and ActiveRecord.
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).
In the following, we will mainly describe how to define a database fixture by extending [[yii\test\ActiveFixture]].
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]]
property. If the latter, the table name will be taken from the `ActiveRecord` class specified by `modelClass`.
```php
<?php
namespace app\tests\fixtures;
use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
}
```
Next, you should override [[yii\test\ActiveFixture::loadSchema()]] to create the table. You may wonder why we need
to create the table when loading a fixture and why we do not work with a database which already has the table. This
is because preparing a complete test database is often very time consuming and in most test cases, only a very tiny part
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;
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
return [
'user1' => [
'username' => 'lmayert',
'email' => 'strosin.vernice@jerde.com',
'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
],
'user2' => [
'username' => 'napoleon69',
'email' => 'aileen.barton@heaneyschumm.com',
'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
],
];
```
You may give an alias to a row so that later in your test, you may refer to the row via the alias. In the above example,
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
values into the rows when the fixture is being loaded.
> Tip: You may customize the location of the data file by setting the [[yii\test\ActiveFixture::dataFile]] property.
> You may also override [[yii\test\ActiveFixture::getData()]] to provide the data.
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,
```php
namespace app\tests\fixtures;
use yii\test\ActiveFixture;
class UserProfileFixture extends ActiveFixture
{
public $modelClass = 'app\models\UserProfile';
public $depends = ['app\tests\fixtures\UserFixture'];
}
```
Using Fixtures
--------------
Yii provides [[yii\test\FixtureTrait]] which can be plugged into your test classes to let you easily load and access
fixtures. More often you would develop your test cases by using the `yii2-codeception` extension
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 your unit test class extending [[yii\codeception\DbTestCase]] (or [[yii\codeception\TestCase]] if you are NOT
testing DB-related features), declare which fixtures you want to use in the [[yii\testFixtureTrait::fixtures()|fixtures()]] method.
For example,
```php
namespace app\tests\unit\models;
use yii\codeception\DbTestCase;
use app\tests\fixtures\UserProfileFixture;
class UserProfileTest extends DbTestCase
{
protected function fixtures()
{
return [
'profiles' => UserProfileFixture::className(),
];
}
// ...test methods...
}
```
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
being loaded, all its dependent fixtures will be automatically loaded first. In the above example, because
`UserProfileFixture` depends on `UserFixture`, when running any test method in the test class,
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
a fixture. The configuration array will let you customize the fixture properties when the fixture is loaded.
You may also assign an alias to a fixture. In the above example, the `UserProfileFixture` is aliased as `profiles`.
In the test methods, you may then access a fixture object using its alias. For example, `$this->profiles` will
return the `UserProfileFixture` object.
Because `UserProfileFixture` extends from `ActiveFixture`, you may further use the following syntax to access
the data provided by the fixture:
```php
// returns the data row aliased as 'user1'
$row = $this->profiles['user1'];
// returns the UserProfile model corresponding to the data row aliased as 'user1'
$profile = $this->profiles('user1');
// traverse every data row in the fixture
foreach ($this->profiles as $row) ...
```
> Info: `$this->profiles` is still of `UserProfileFixture` type. The above access features are implemented
> 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 loads fixtures, it will
first load global fixtures and then non-global ones.
By default, [[yii\codeception\DbTestCase]] already declares `InitDbFixture` in its `globalFixtures()` method.
This means you only need to work with `@app/tests/fixtures/initdb.php` to set up your skeleton test database,
and you can then focus on developing each individual test case and the corresponding fixtures.
<?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,6 +5,7 @@ namespace yii\codeception;
use Yii;
use yii\base\InvalidConfigException;
use Codeception\TestCase\Test;
use yii\test\FixtureTrait;
/**
* TestCase is the base class for all codeception unit tests
......@@ -14,6 +15,8 @@ use Codeception\TestCase\Test;
*/
class TestCase extends Test
{
use FixtureTrait;
/**
* @var array|string the application configuration that will be used for creating an application instance for each test.
* You can use a string to represent the file path or path alias of a configuration file.
......@@ -29,6 +32,7 @@ class TestCase extends Test
{
parent::setUp();
$this->mockApplication();
$this->loadFixtures();
}
/**
......@@ -36,6 +40,7 @@ class TestCase extends Test
*/
protected function tearDown()
{
$this->unloadFixtures();
$this->destroyApplication();
parent::tearDown();
}
......
<?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
* 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.
* @return Command the command object itself
*/
public function dropPrimaryKey($name, $table)
{
......
<?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;
use yii\db\TableSchema;
/**
* ActiveFixture represents a fixture backed up by a [[modelClass|ActiveRecord class]] or a [[tableName|database table]].
*
* Either [[modelClass]] or [[tableName]] must be set. And you should normally override [[loadSchema()]]
* 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.
*
* When the fixture is being loaded, it will first call [[loadSchema()]] to initialize the database schema.
* 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>
* @since 2.0
*/
class ActiveFixture extends BaseActiveFixture
{
/**
* @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]].
* @see modelClass
*/
public $tableName;
/**
* @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`,
* where `FixturePath` stands for the directory containing this fixture class, and `TableName` stands for the
* name of the table associated with this fixture.
*/
public $dataFile;
/**
* @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 $resetTable = false;
/**
* @var TableSchema the table schema for the table associated with this fixture
*/
private $_table;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if (!isset($this->modelClass) && !isset($this->tableName)) {
throw new InvalidConfigException('Either "modelClass" or "tableName" must be set.');
}
}
/**
* @return TableSchema the schema information of the database table associated with this fixture.
* @throws \yii\base\InvalidConfigException if the table does not exist
*/
public function getTableSchema()
{
if ($this->_table !== null) {
return $this->_table;
}
$db = $this->db;
$tableName = $this->tableName;
if ($tableName === null) {
/** @var \yii\db\ActiveRecord $modelClass */
$modelClass = $this->modelClass;
$tableName = $modelClass::tableName();
}
$this->_table = $db->getSchema()->getTableSchema($tableName);
if ($this->_table === null) {
throw new InvalidConfigException("Table does not exist: {$tableName}");
}
return $this->_table;
}
/**
* 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()
{
$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;
}
}
/**
* Returns the fixture data.
*
* This method is called by [[loadData()]] to get the needed fixture data.
*
* 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.
*
* 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.
*/
protected function getData()
{
if ($this->dataFile === false) {
return [];
}
if ($this->dataFile !== null) {
$dataFile = Yii::getAlias($this->dataFile);
} else {
$class = new \ReflectionClass($this);
$dataFile = dirname($class->getFileName()) . '/data/' . $this->getTableSchema()->fullName . '.php';
}
return is_file($dataFile) ? require($dataFile) : [];
}
/**
* Removes all existing data from the specified table and resets sequence number if any.
* This method is called before populating fixture data into the table associated with this fixture.
*/
protected function resetTable()
{
$table = $this->getTableSchema();
$this->db->createCommand()->delete($table->fullName)->execute();
if ($table->sequenceName !== null) {
$this->db->createCommand()->resetSequence($table->fullName, 1)->execute();
}
}
}
<?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 [];
}
}
<?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\InvalidConfigException;
use yii\db\Connection;
/**
* DbFixture is the base class for DB-related fixtures.
*
* DbFixture provides the [[db]] connection as well as a set of commonly used DB manipulation methods.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class DbFixture extends Fixture
{
/**
* @var Connection|string the DB connection object or the application component ID of the DB connection.
* After the DbFixture object is created, if you want to change this property, you should only assign it
* with a DB connection object.
*/
public $db = 'db';
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if (is_string($this->db)) {
$this->db = Yii::$app->getComponent($this->db);
}
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.");
}
}
/**
* 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 execute($sql, $params = [])
{
$this->db->createCommand($sql)->execute($params);
}
/**
* 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 insert($table, $columns)
{
$this->db->createCommand()->insert($table, $columns)->execute();
}
/**
* 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 batchInsert($table, $columns, $rows)
{
$this->db->createCommand()->batchInsert($table, $columns, $rows)->execute();
}
/**
* Creates and executes an UPDATE SQL statement.
* The method will properly escape the column names and bind the values to be updated.
* @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 update($table, $columns, $condition = '', $params = [])
{
$this->db->createCommand()->update($table, $columns, $condition, $params)->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();
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\test;
use yii\base\Component;
/**
* Fixture represents a fixed state of a test environment.
*
* Each fixture instance represents a particular aspect of a test environment. For example,
* you may use `UserFixture` to initialize the user database table with a set of known data. You may
* load the fixture when running every test method so that the user table always contains the fixed data
* and thus allows your test predictable and repeatable.
*
* A fixture may depend on other fixtures, specified via the [[depends]] property. When a fixture is being loaded,
* its dependent fixtures will be automatically loaded BEFORE the fixture; and when the fixture is being unloaded,
* its dependent fixtures will be unloaded AFTER the fixture.
*
* You should normally override [[load()]] to specify how to set up a fixture; and override [[unload()]]
* for clearing up a fixture.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Fixture extends Component
{
/**
* @var array the fixtures that this fixture depends on. This must be a list of the dependent
* fixture class names.
*/
public $depends = [];
/**
* Loads the fixture.
* This method is called before performing every test method.
* You should override this method with concrete implementation about how to set up the fixture.
*/
public function load()
{
}
/**
* This method is called BEFORE any fixture data is loaded for the current test.
*/
public function beforeLoad()
{
}
/**
* This method is called AFTER all fixture data have been loaded for the current test.
*/
public function afterLoad()
{
}
/**
* Unloads the fixture.
* This method is called after every test method finishes.
* You may override this method to perform necessary cleanup work for the fixture.
*/
public function unload()
{
}
}
<?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\InvalidConfigException;
use yii\base\UnknownMethodException;
use yii\base\UnknownPropertyException;
/**
* FixtureTrait provides functionalities for loading, unloading and accessing fixtures for a test case.
*
* By using FixtureTrait, a test class will be able to specify which fixtures to load by overriding
* the [[fixtures()]] method. It can then load and unload the fixtures using [[loadFixtures()]] and [[unloadFixtures()]].
* Once a fixture is loaded, it can be accessed like an object property, thanks to the PHP `__get()` magic method.
* Also, if the fixture is an instance of [[ActiveFixture]], you will be able to access AR models
* through the syntax `$this->fixtureName('model name')`.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
trait FixtureTrait
{
/**
* @var array the list of fixture objects available for the current test.
* The array keys are the corresponding fixture class names.
* The fixtures are listed in their dependency order. That is, fixture A is listed before B
* if B depends on A.
*/
private $_fixtures;
/**
* @var array the fixture class names indexed by the corresponding fixture names (aliases).
*/
private $_fixtureAliases;
/**
* Returns the value of an object property.
*
* Do not call this method directly as it is a PHP magic method that
* will be implicitly called when executing `$value = $object->property;`.
* @param string $name the property name
* @return mixed the property value
* @throws UnknownPropertyException if the property is not defined
*/
public function __get($name)
{
$fixture = $this->getFixture($name);
if ($fixture !== null) {
return $fixture;
} else {
throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}
}
/**
* Calls the named method which is not a class method.
*
* Do not call this method directly as it is a PHP magic method that
* will be implicitly called when an unknown method is being invoked.
* @param string $name the method name
* @param array $params method parameters
* @throws UnknownMethodException when calling unknown method
* @return mixed the method return value
*/
public function __call($name, $params)
{
$fixture = $this->getFixture($name);
if ($fixture instanceof ActiveFixture) {
return $fixture->getModel(reset($params));
} else {
throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()");
}
}
/**
* Declares the fixtures that are needed by the current test case.
* The return value of this method must be an array of fixture configurations. For example,
*
* ```php
* [
* // anonymous fixture
* PostFixture::className(),
* // "users" fixture
* 'users' => UserFixture::className(),
* // "cache" fixture with configuration
* 'cache' => [
* 'class' => CacheFixture::className(),
* 'host' => 'xxx',
* ],
* ]
* ```
*
* 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
*/
protected function fixtures()
{
return [];
}
/**
* 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.
* 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.
* @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among
* the fixtures is detected.
*/
protected function loadFixtures($fixtures = null)
{
if ($fixtures === null) {
$fixtures = array_merge($this->globalFixtures(), $this->fixtures());
}
// normalize fixture configurations
$config = []; // configuration provided in test case
$this->_fixtureAliases = [];
foreach ($fixtures as $name => $fixture) {
if (!is_array($fixture)) {
$fixtures[$name] = $fixture = ['class' => $fixture];
} elseif (!isset($fixture['class'])) {
throw new InvalidConfigException("You must specify 'class' for the fixture '$name'.");
}
$config[$fixture['class']] = $fixture;
$this->_fixtureAliases[$name] = $fixture['class'];
}
// create fixture instances
$this->_fixtures = [];
$stack = array_reverse($fixtures);
while (($fixture = array_pop($stack)) !== null) {
if ($fixture instanceof Fixture) {
$class = get_class($fixture);
unset($this->_fixtures[$class]); // unset so that the fixture is added to the last in the next line
$this->_fixtures[$class] = $fixture;
} else {
$class = $fixture['class'];
if (!isset($this->_fixtures[$class])) {
$this->_fixtures[$class] = false;
$stack[] = $fixture = Yii::createObject($fixture);
foreach ($fixture->depends as $dep) {
// need to use the configuration provided in test case
$stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep];
}
} elseif ($this->_fixtures[$class] === false) {
throw new InvalidConfigException("A circular dependency is detected for fixture '$class'.");
}
}
}
// load fixtures
/** @var Fixture $fixture */
foreach ($this->_fixtures as $fixture) {
$fixture->beforeLoad();
}
foreach ($this->_fixtures as $fixture) {
$fixture->load();
}
foreach ($this->_fixtures as $fixture) {
$fixture->afterLoad();
}
}
/**
* Unloads all existing fixtures.
*/
protected function unloadFixtures()
{
/** @var Fixture $fixture */
foreach (array_reverse($this->_fixtures) as $fixture) {
$fixture->unload();
}
}
/**
* @return array the loaded fixtures for the current test case
*/
protected function getFixtures()
{
return $this->_fixtures;
}
/**
* Returns the named fixture.
* @param string $name the fixture alias or class name
* @return Fixture the fixture object, or null if the named fixture does not exist.
*/
protected function getFixture($name)
{
$class = isset($this->_fixtureAliases[$name]) ? $this->_fixtureAliases[$name] : $name;
return isset($this->_fixtures[$class]) ? $this->_fixtures[$class] : null;
}
}
<?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);
}
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\test;
use yii\test\ActiveFixture;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\framework\db\DatabaseTestCase;
class Customer extends ActiveRecord
{
public static function tableName()
{
return 'tbl_customer2';
}
}
class CustomerFixture extends ActiveFixture
{
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
{
use FixtureTrait;
public function setUp()
{
$this->loadFixtures();
}
public function tearDown()
{
$this->unloadFixtures();
}
protected function fixtures()
{
return [
'customers' => CustomerFixture::className(),
];
}
protected function globalFixtures()
{
return [
InitDbFixture::className(),
];
}
}
/**
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ActiveFixtureTest extends DatabaseTestCase
{
public function setUp()
{
parent::setUp();
\Yii::$app->setComponent('db', $this->getConnection());
Customer::$db = $this->getConnection();
}
public function tearDown()
{
parent::tearDown();
}
public function testGetData()
{
$test = new MyDbTestCase();
$test->setUp();
$fixture = $test->customers;
$this->assertEquals(CustomerFixture::className(), get_class($fixture));
$this->assertEquals(2, count($fixture));
$this->assertEquals(1, $fixture['customer1']['id']);
$this->assertEquals('customer1@example.com', $fixture['customer1']['email']);
$this->assertEquals(2, $fixture['customer2']['id']);
$this->assertEquals('customer2@example.com', $fixture['customer2']['email']);
}
public function testGetModel()
{
$test = new MyDbTestCase();
$test->setUp();
$fixture = $test->customers;
$this->assertEquals(Customer::className(), get_class($fixture->getModel('customer1')));
$this->assertEquals(1, $fixture->getModel('customer1')->id);
$this->assertEquals('customer1@example.com', $fixture->getModel('customer1')->email);
$this->assertEquals(2, $fixture->getModel('customer2')->id);
$this->assertEquals('customer2@example.com', $fixture->getModel('customer2')->email);
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\test;
use yii\test\Fixture;
use yii\test\FixtureTrait;
use yiiunit\TestCase;
class Fixture1 extends Fixture
{
public $depends = ['yiiunit\framework\test\Fixture2'];
public function load()
{
MyTestCase::$load .= '1';
}
public function unload()
{
MyTestCase::$unload .= '1';
}
}
class Fixture2 extends Fixture
{
public $depends = ['yiiunit\framework\test\Fixture3'];
public function load()
{
MyTestCase::$load .= '2';
}
public function unload()
{
MyTestCase::$unload .= '2';
}
}
class Fixture3 extends Fixture
{
public function load()
{
MyTestCase::$load .= '3';
}
public function unload()
{
MyTestCase::$unload .= '3';
}
}
class MyTestCase
{
use FixtureTrait;
public $scenario = 1;
public static $load;
public static $unload;
public function setUp()
{
$this->loadFixtures();
}
public function tearDown()
{
$this->unloadFixtures();
}
public function fetchFixture($name)
{
return $this->getFixture($name);
}
protected function fixtures()
{
switch ($this->scenario) {
case 0: return [];
case 1: return [
'fixture1' => Fixture1::className(),
];
case 2: return [
'fixture2' => Fixture2::className(),
];
case 3: return [
'fixture3' => Fixture3::className(),
];
case 4: return [
'fixture1' => Fixture1::className(),
'fixture2' => Fixture2::className(),
];
case 5: return [
'fixture2' => Fixture2::className(),
'fixture3' => Fixture3::className(),
];
case 6: return [
'fixture1' => Fixture1::className(),
'fixture3' => Fixture3::className(),
];
case 7:
default: return [
'fixture1' => Fixture1::className(),
'fixture2' => Fixture2::className(),
'fixture3' => Fixture3::className(),
];
}
}
}
class FixtureTest extends TestCase
{
public function testDependencies()
{
foreach ($this->getDependencyTests() as $scenario => $result) {
$test = new MyTestCase();
$test->scenario = $scenario;
$test->setUp();
foreach ($result as $name => $loaded) {
$this->assertEquals($loaded, $test->fetchFixture($name) !== null, "Verifying scenario $scenario fixture $name");
}
}
}
public function testLoadSequence()
{
foreach ($this->getLoadSequenceTests() as $scenario => $result) {
$test = new MyTestCase();
$test->scenario = $scenario;
MyTestCase::$load = '';
MyTestCase::$unload = '';
$test->setUp();
$this->assertEquals($result[0], MyTestCase::$load, "Verifying scenario $scenario load sequence");
$test->tearDown();
$this->assertEquals($result[1], MyTestCase::$unload, "Verifying scenario $scenario unload sequence");
}
}
protected function getDependencyTests()
{
return [
0 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false],
1 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => false],
2 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => false],
3 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => true],
4 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => false],
5 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => true],
6 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => true],
7 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => true],
];
}
protected function getLoadSequenceTests()
{
return [
0 => ['', ''],
1 => ['321', '123'],
2 => ['32', '23'],
3 => ['3', '3'],
4 => ['321', '123'],
5 => ['32', '23'],
6 => ['321', '123'],
7 => ['321', '123'],
];
}
}
<?php
return [
'customer1' => [
'email' => 'customer1@example.com',
'name' => 'customer1',
'address' => 'address1',
'status' => 1,
],
'customer2' => [
'email' => 'customer2@example.com',
'name' => 'customer2',
'address' => 'address2',
'status' => 2,
],
];
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