Commit b496a04f by Qiang Xue

refactored fixture feature.

parent 12eca516
...@@ -25,10 +25,6 @@ return [ ...@@ -25,10 +25,6 @@ return [
], ],
], ],
'db' => $db, 'db' => $db,
'fixture' => [
'class' => 'yii\test\DbFixtureManager',
'basePath' => '@tests/unit/fixtures',
],
], ],
'params' => $params, 'params' => $params,
]; ];
...@@ -5,10 +5,6 @@ return yii\helpers\ArrayHelper::merge( ...@@ -5,10 +5,6 @@ return yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../_config.php'), require(__DIR__ . '/../_config.php'),
[ [
'components' => [ 'components' => [
'fixture' => [
'class' => 'yii\test\DbFixtureManager',
'basePath' => '@tests/unit/fixtures',
],
'db' => [ 'db' => [
'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit', 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit',
], ],
......
...@@ -5,9 +5,9 @@ Fixtures are important part of testing. Their main purpose is to set up the envi ...@@ -5,9 +5,9 @@ Fixtures are important part of testing. Their main purpose is to set up the envi
so that your tests are repeatable and run in an expected way. Yii provides a fixture framework that allows 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. 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 represents
of [[yii\test\Fixture]] or its child class. It represents a particular aspect of a test environment. For example, a particular aspect of a test environment and is an instance of [[yii\test\Fixture]] or its child class. For example,
you may define `UserFixture` to create the user table and populate it with some known data. You load one or multiple you may use `UserFixture` to make sure the user DB table contains a fixed set of 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,13 +22,7 @@ To define a fixture, create a new class by extending [[yii\test\Fixture]] or [[y ...@@ -22,13 +22,7 @@ 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]], you should normally override the [[yii\test\Fixture::load()]] method The following code defines a fixture about the `User` ActiveRecord and the corresponding user table.
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
<?php <?php
...@@ -42,33 +36,12 @@ class UserFixture extends ActiveFixture ...@@ -42,33 +36,12 @@ class UserFixture extends ActiveFixture
} }
``` ```
Next, you should override [[yii\test\ActiveFixture::loadSchema()]] to create the table. You may wonder why we need > Tip: Each `ActiveFixture` is about preparing a DB table for testing purpose. You may specify the table
to create the table when loading a fixture and why we do not work with a database which already has the table. This > by setting either the [[yii\test\ActiveFixture::tableName]] property or the [[yii\test\ActiveFixture::modelClass]]
is because preparing a complete test database is often very time consuming and in most test cases, only a very tiny part > property. If the latter, the table name will be taken from the `ActiveRecord` class specified by `modelClass`.
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; The fixture data for an `ActiveFixture` fixture is usually provided in a file located at `FixturePath/data/TableName.php`,
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` 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 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 `@app/tests/fixtures/data/tbl_user.php`. The data file should return an array of data rows
...@@ -115,20 +88,25 @@ class UserProfileFixture extends ActiveFixture ...@@ -115,20 +88,25 @@ class UserProfileFixture extends ActiveFixture
public $modelClass = 'app\models\UserProfile'; public $modelClass = 'app\models\UserProfile';
public $depends = ['app\tests\fixtures\UserFixture']; public $depends = ['app\tests\fixtures\UserFixture'];
} }
```
In the above, we have shown how to define a fixture about a DB table. To define a fixture not related with DB
(e.g. a fixture about certain files and directories), you may extend from the more general base class
[[yii\test\Fixture]] and override the [[yii\test\Fixture::load()|load()]] and [[yii\test\Fixture::unload()|unload()]] methods.
Using Fixtures Using Fixtures
-------------- --------------
Yii provides [[yii\test\FixtureTrait]] which can be plugged into your test classes to let you easily load and access If you are using [CodeCeption](http://codeception.com/) to test your code, you should consider using
fixtures. More often you would develop your test cases by using the `yii2-codeception` extension the `yii2-codeception` extension which has the built-in support for loading and accessing fixtures.
which uses [[yii\test\FixtureTrait]] and has the built-in support for the loading and accessing fixtures. If you are using other testing frameworks, you may use [[yii\test\FixtureTrait]] in your test cases
to achieve the same goal.
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\DbTestCase]] (or [[yii\codeception\TestCase]] if you are NOT In your unit test class extending [[yii\codeception\DbTestCase]] or [[yii\codeception\TestCase]],
testing DB-related features), declare which fixtures you want to use in the [[yii\testFixtureTrait::fixtures()|fixtures()]] method. declare which fixtures you want to use in the [[yii\testFixtureTrait::fixtures()|fixtures()]] method. For example,
For example,
```php ```php
namespace app\tests\unit\models; namespace app\tests\unit\models;
...@@ -182,15 +160,31 @@ Defining and Using Global Fixtures ...@@ -182,15 +160,31 @@ Defining and Using Global Fixtures
---------------------------------- ----------------------------------
The fixtures described above are mainly used by individual test cases. In most cases, you also need some global 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 fixtures that are applied to ALL or many test cases. An example is [[yii\test\InitDbFixture]] which does
set up a skeleton test database and toggle database integrity checks when applying other DB fixtures. two things:
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. * Perform some common initialization tasks by executing a script located at `@app/tests/fixtures/initdb.php`;
* Disable the database integrity check before loading other DB fixtures, and re-enable it after other DB fixtures are unloead.
Using global fixtures is similar to using non-global ones. The only difference is that you declare these fixtures 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 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. first load global fixtures and then non-global ones.
By default, [[yii\codeception\DbTestCase]] already declares `InitDbFixture` in its `globalFixtures()` method. 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, This means you only need to work with `@app/tests/fixtures/initdb.php` if you want to do some initialization work
and you can then focus on developing each individual test case and the corresponding fixtures. before each test. You may otherwise simply focus on developing each individual test case and the corresponding fixtures.
Summary
-------
In the above, we have described how to define and use fixtures. Below we summarize the typical workflow
of running unit tests related with DB:
1. Use `yii migrate` tool to upgrade your test database to the latest version;
2. Run a test case:
- Load fixtures: clean up the relevant DB tables and populate them with fixture data;
- Perform the actual test;
- Unload fixtures.
3. Repeat 2 until all tests finish.
...@@ -21,7 +21,6 @@ Yii Framework 2 Change Log ...@@ -21,7 +21,6 @@ Yii Framework 2 Change Log
- Bug #1686: ActiveForm is creating duplicated messages in error summary (qiangxue) - Bug #1686: ActiveForm is creating duplicated messages in error summary (qiangxue)
- Bug #1704: Incorrect regexp is used in `Inflector::camelize()` (qiangxue) - Bug #1704: Incorrect regexp is used in `Inflector::camelize()` (qiangxue)
- Bug #1710: OpenId auth client does not request required attributes correctly (klimov-paul) - Bug #1710: OpenId auth client does not request required attributes correctly (klimov-paul)
- Bug #1733: Incorrect code about `$_modelClasses` in `DbFixtureManager` (qiangxue)
- Bug #1798: Fixed label attributes for array fields (zhuravljov) - Bug #1798: Fixed label attributes for array fields (zhuravljov)
- Bug #1800: Better check for `$_SERVER['HTTPS']` in `yii\web\Request::getIsSecureConnection()` (ginus, samdark) - Bug #1800: Better check for `$_SERVER['HTTPS']` in `yii\web\Request::getIsSecureConnection()` (ginus, samdark)
- Bug #1827: Debugger toolbar is loaded twice if an action is calling `run()` to execute another action (qiangxue) - Bug #1827: Debugger toolbar is loaded twice if an action is calling `run()` to execute another action (qiangxue)
......
...@@ -174,7 +174,6 @@ return [ ...@@ -174,7 +174,6 @@ return [
'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php', 'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php',
'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php', 'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php',
'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php', 'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php',
'yii\test\DbFixtureManager' => YII_PATH . '/test/DbFixtureManager.php',
'yii\test\DbTestTrait' => YII_PATH . '/test/DbTestTrait.php', 'yii\test\DbTestTrait' => YII_PATH . '/test/DbTestTrait.php',
'yii\validators\BooleanValidator' => YII_PATH . '/validators/BooleanValidator.php', 'yii\validators\BooleanValidator' => YII_PATH . '/validators/BooleanValidator.php',
'yii\validators\CompareValidator' => YII_PATH . '/validators/CompareValidator.php', 'yii\validators\CompareValidator' => YII_PATH . '/validators/CompareValidator.php',
......
...@@ -11,7 +11,6 @@ use Yii; ...@@ -11,7 +11,6 @@ use Yii;
use yii\console\Controller; use yii\console\Controller;
use yii\console\Exception; use yii\console\Exception;
use yii\helpers\FileHelper; use yii\helpers\FileHelper;
use yii\test\DbTestTrait;
use yii\helpers\Console; use yii\helpers\Console;
/** /**
...@@ -309,7 +308,7 @@ class FixtureController extends Controller ...@@ -309,7 +308,7 @@ class FixtureController extends Controller
/** /**
* @param array $fixtures * @param array $fixtures
* @return array Array of found fixtures. These may differer from input parameter as not all fixtures may exists. * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
*/ */
private function findFixtures(array $fixtures) private function findFixtures(array $fixtures)
{ {
......
...@@ -15,13 +15,11 @@ use yii\db\TableSchema; ...@@ -15,13 +15,11 @@ 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. And you should normally override [[loadSchema()]] * Either [[modelClass]] or [[tableName]] must be set. You should also provide fixture data in the file
* to set up the necessary database schema (e.g. creating the table, view, trigger, etc.) * specified by [[dataFile]] or overriding [[getData()]] if you want to use code to generate the fixture data.
* 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. * When the fixture is being loaded, it will first call [[resetTable()]] to remove any existing data in the table.
* It will then call [[loadData()]] to populate the table with the fixture data. * It will then populate the table with the data returned by [[getData()]].
* *
* After the fixture is loaded, you can access the loaded data via the [[data]] property. If you set [[modelClass]], * 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()]]. * you will also be able to retrieve an instance of [[modelClass]] with the populated data via [[getModel()]].
...@@ -38,22 +36,13 @@ class ActiveFixture extends BaseActiveFixture ...@@ -38,22 +36,13 @@ class ActiveFixture extends BaseActiveFixture
*/ */
public $tableName; public $tableName;
/** /**
* @var string the file path or path alias of the data file that contains the fixture data * @var string|boolean 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`, * to be returned by [[getData()]]. 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. * name of the table associated with this fixture. You can set this property to be false to prevent loading any data.
*/ */
public $dataFile; 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 * @var TableSchema the table schema for the table associated with this fixture
*/ */
private $_table; private $_table;
...@@ -70,42 +59,20 @@ class ActiveFixture extends BaseActiveFixture ...@@ -70,42 +59,20 @@ class ActiveFixture extends BaseActiveFixture
} }
/** /**
* @return TableSchema the schema information of the database table associated with this fixture. * Loads the fixture.
* @throws \yii\base\InvalidConfigException if the table does not exist *
* The default implementation will first clean up the table by calling [[resetTable()]].
* It will then populate the table with the data returned by [[getData()]].
*
* If you override this method, you should consider calling the parent implementation
* so that the data returned by [[getData()]] can be populated into the table.
*/ */
public function getTableSchema() public function load()
{ {
if ($this->_table !== null) { $this->resetTable();
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(); $table = $this->getTableSchema();
if ($this->resetTable) {
$this->resetTable();
}
foreach ($this->getData() as $alias => $row) { foreach ($this->getData() as $alias => $row) {
$this->db->createCommand()->insert($table->fullName, $row)->execute(); $this->db->createCommand()->insert($table->fullName, $row)->execute();
if ($table->sequenceName !== null) { if ($table->sequenceName !== null) {
...@@ -123,8 +90,6 @@ class ActiveFixture extends BaseActiveFixture ...@@ -123,8 +90,6 @@ class ActiveFixture extends BaseActiveFixture
/** /**
* Returns the fixture data. * 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 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.
* *
...@@ -147,7 +112,7 @@ class ActiveFixture extends BaseActiveFixture ...@@ -147,7 +112,7 @@ class ActiveFixture extends BaseActiveFixture
} }
/** /**
* Removes all existing data from the specified table and resets sequence number if any. * Removes all existing data from the specified table and resets sequence number to 1 (if any).
* This method is called before populating fixture data into the table associated with this fixture. * This method is called before populating fixture data into the table associated with this fixture.
*/ */
protected function resetTable() protected function resetTable()
...@@ -158,4 +123,30 @@ class ActiveFixture extends BaseActiveFixture ...@@ -158,4 +123,30 @@ class ActiveFixture extends BaseActiveFixture
$this->db->createCommand()->resetSequence($table->fullName, 1)->execute(); $this->db->createCommand()->resetSequence($table->fullName, 1)->execute();
} }
} }
/**
* @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;
}
} }
...@@ -27,16 +27,6 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate ...@@ -27,16 +27,6 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate
*/ */
public $modelClass; 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). * @var array the data rows. Each array element represents one row of data (column name => column value).
*/ */
public $data = []; public $data = [];
...@@ -47,21 +37,8 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate ...@@ -47,21 +37,8 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate
/** /**
* @inheritdoc
*/
public function load()
{
if ($this->loadSchema) {
$this->loadSchema();
}
if ($this->loadData) {
$this->loadData();
}
}
/**
* Returns the AR model by the specified model name. * Returns the AR model by the specified model name.
* A model name is the key of the corresponding data row returned by [[loadData()]]. * A model name is the key of the corresponding data row in [[data]].
* @param string $name the model name. * @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 * @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. * @throws \yii\base\InvalidConfigException if [[modelClass]] is not set.
...@@ -89,23 +66,4 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate ...@@ -89,23 +66,4 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate
} }
return $this->_models[$name] = $modelClass::find($keys); 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 [];
}
} }
...@@ -14,7 +14,7 @@ use yii\db\Connection; ...@@ -14,7 +14,7 @@ use yii\db\Connection;
/** /**
* DbFixture is the base class for DB-related fixtures. * 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. * DbFixture provides the [[db]] connection to be used by DB fixtures.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
...@@ -41,256 +41,4 @@ abstract class DbFixture extends Fixture ...@@ -41,256 +41,4 @@ abstract class DbFixture extends Fixture
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.");
} }
} }
/**
* 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;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
use yii\db\Connection;
/**
* DbFixtureManager manages database fixtures during tests.
*
* A fixture represents a list of rows for a specific table. For a test method,
* using a fixture means that at the beginning of the method, the table has and only
* has the rows that are given in the fixture. Therefore, the table's state is
* predictable.
*
* A fixture is represented as a PHP script whose name (without suffix) is the
* same as the table name (if schema name is needed, it should be prefixed to
* the table name). The PHP script returns an array representing a list of table
* rows. Each row is an associative array of column values indexed by column names.
*
* Fixtures must be stored under the [[basePath]] directory. The directory
* may contain a file named `init.php` which will be executed before any fixture is loaded.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class DbFixtureManager extends Component
{
/**
* @var string the init script file that should be executed before running each test.
* This should be a path relative to [[basePath]].
*/
public $initScript = 'init.php';
/**
* @var string the base path containing all fixtures. This can be either a directory path or path alias.
*/
public $basePath = '@app/tests/fixtures';
/**
* @var Connection|string the DB connection object or the application component ID of the DB connection.
* After the DbFixtureManager object is created, if you want to change this property, you should only assign it
* with a DB connection object.
*/
public $db = 'db';
/**
* @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 = [''];
private $_rows; // fixture name, row alias => row
private $_models; // fixture name, row alias => record (or class name)
/**
* Loads the specified fixtures.
*
* This method does the following things to load the fixtures:
*
* - Run [[initScript]] if any.
* - Clean up data and models loaded in memory previously.
* - Load each specified fixture by calling [[loadFixture()]].
*
* @param array $fixtures a list of fixtures (fixture name => table name or AR class name) to be loaded.
* Each array element can be either a table name (with schema prefix if needed), or a fully-qualified
* ActiveRecord class name (e.g. `app\models\Post`). An element can be associated with a key
* which will be treated as the fixture name.
* @return array the loaded fixture data (fixture name => table rows)
* @throws InvalidConfigException if a model class specifying a fixture is not an ActiveRecord class.
*/
public function load(array $fixtures = [])
{
$this->basePath = Yii::getAlias($this->basePath);
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.");
}
foreach ($fixtures as $name => $fixture) {
if (strpos($fixture, '\\') !== false) {
$model = new $fixture;
if ($model instanceof ActiveRecord) {
$fixtures[$name] = $model->getTableSchema()->name;
} else {
throw new InvalidConfigException("Fixture '$fixture' must be an ActiveRecord class.");
}
}
}
$this->_rows = $this->_models = [];
$this->checkIntegrity(false);
if (!empty($this->initScript)) {
$initFile = $this->basePath . '/' . $this->initScript;
if (is_file($initFile)) {
require($initFile);
}
}
foreach ($fixtures as $name => $tableName) {
$rows = $this->loadFixture($tableName);
if (is_array($rows)) {
$this->_rows[$name] = $rows;
}
}
$this->checkIntegrity(true);
return $this->_rows;
}
/**
* Loads the fixture for the specified table.
*
* This method does the following tasks to load the fixture for a table:
*
* - Remove existing rows in the table.
* - If there is any auto-incremental column, the corresponding sequence will be reset to 0.
* - If a fixture file is found, it will be executed, and its return value will be treated
* as rows which will then be inserted into the table.
*
* @param string $tableName table name
* @return array|boolean the loaded fixture rows indexed by row aliases (if any).
* False is returned if the table does not have a fixture.
* @throws InvalidConfigException if the specified table does not exist
*/
public function loadFixture($tableName)
{
$table = $this->db->getSchema()->getTableSchema($tableName);
if ($table === null) {
throw new InvalidConfigException("Table does not exist: $tableName");
}
$this->db->createCommand()->delete($tableName)->execute();
$this->db->createCommand()->resetSequence($tableName, 1)->execute();
$fileName = $this->basePath . '/' . $tableName . '.php';
if (!is_file($fileName)) {
return false;
}
$rows = [];
foreach (require($fileName) as $alias => $row) {
$this->db->createCommand()->insert($tableName, $row)->execute();
if ($table->sequenceName !== null) {
foreach ($table->primaryKey as $pk) {
if (!isset($row[$pk])) {
$row[$pk] = $this->db->getLastInsertID($table->sequenceName);
break;
}
}
}
$rows[$alias] = $row;
}
return $rows;
}
/**
* Returns the fixture data rows.
* The rows will have updated primary key values if the primary key is auto-incremental.
* @param string $fixtureName the fixture name
* @return array the fixture data rows. False is returned if there is no such fixture data.
*/
public function getRows($fixtureName)
{
return isset($this->_rows[$fixtureName]) ? $this->_rows[$fixtureName] : false;
}
/**
* Returns the specified ActiveRecord instance in the fixture data.
* @param string $fixtureName the fixture name
* @param string $modelName the alias for the fixture data row
* @return ActiveRecord the ActiveRecord instance. Null is returned if there is no such fixture row.
*/
public function getModel($fixtureName, $modelName)
{
if (!isset($this->_rows[$fixtureName][$modelName])) {
return null;
}
if (isset($this->_models[$fixtureName][$modelName])) {
return $this->_models[$fixtureName][$modelName];
}
$row = $this->_rows[$fixtureName][$modelName];
/** @var ActiveRecord $modelClass */
$modelClass = $this->_models[$fixtureName];
/** @var ActiveRecord $model */
$model = new $modelClass;
$keys = [];
foreach ($model->primaryKey() as $key) {
$keys[$key] = isset($row[$key]) ? $row[$key] : null;
}
return $this->_models[$fixtureName][$modelName] = $modelClass::find($keys);
}
/**
* Enables or disables database integrity check.
* This method may be used to temporarily turn off foreign constraints check.
* @param boolean $check whether to enable database integrity check
*/
public function checkIntegrity($check)
{
foreach ($this->schemas as $schema) {
$this->db->createCommand()->checkIntegrity($check, $schema)->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;
/**
* DbTestTrait implements the commonly used methods for setting up and accessing fixture data.
*
* To use DbTestTrait, call the [[loadFixtures()]] method in the setup method in a test case class.
* The specified fixtures will be loaded and accessible through [[getFixtureData()]] and [[getFixtureModel()]].
*
* For example,
*
* ~~~
* use yii\test\DbTestTrait;
* use yii\codeception\TestCase;
* use app\models\Post;
* use app\models\User;
*
* class PostTestCase extends TestCase
* {
* use DbTestTrait;
*
* protected function setUp()
* {
* parent::setUp();
*
* $this->loadFixtures([
* 'posts' => Post::className(),
* 'users' => User::className(),
* ]);
* }
* }
* ~~~
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
trait DbTestTrait
{
/**
* Loads the specified fixtures.
*
* This method should typically be called in the setup method of test cases so that
* the fixtures are loaded before running each test method.
*
* This method does the following things:
*
* - Run [[DbFixtureManager::initScript]] if it is found under [[DbFixtureManager::basePath]].
* - Clean up data and models loaded in memory previously.
* - Load each specified fixture:
* * Truncate the corresponding table.
* * If a fixture file named `TableName.php` is found under [[DbFixtureManager::basePath]],
* the file will be executed, and the return value will be treated as rows which will
* then be inserted into the table.
*
* @param array $fixtures a list of fixtures (fixture name => table name or AR class name) to be loaded.
* Each array element can be either a table name (with schema prefix if needed), or a fully-qualified
* ActiveRecord class name (e.g. `app\models\Post`). An element can be optionally associated with a key
* which will be treated as the fixture name. For example,
*
* ~~~
* [
* 'tbl_comment',
* 'users' => 'tbl_user', // 'users' is the fixture name, 'tbl_user' is a table name
* 'posts' => 'app\models\Post, // 'app\models\Post' is a model class name
* ]
* ~~~
*
* @return array the loaded fixture data (fixture name => table rows)
*/
public function loadFixtures(array $fixtures = [])
{
return $this->getFixtureManager()->load($fixtures);
}
/**
* Returns the DB fixture manager.
* @return DbFixtureManager the DB fixture manager
*/
public function getFixtureManager()
{
return Yii::$app->getComponent('fixture');
}
/**
* Returns the table rows of the named fixture.
* @param string $fixtureName the fixture name.
* @return array the named fixture table rows. False is returned if there is no such fixture data.
*/
public function getFixtureRows($fixtureName)
{
return $this->getFixtureManager()->getRows($fixtureName);
}
/**
* Returns the named AR instance corresponding to the named fixture.
* @param string $fixtureName the fixture name.
* @param string $modelName the name of the fixture data row
* @return \yii\db\ActiveRecord the named AR instance corresponding to the named fixture.
* Null is returned if there is no such fixture or the record cannot be found.
*/
public function getFixtureModel($fixtureName, $modelName)
{
return $this->getFixtureManager()->getModel($fixtureName, $modelName);
}
}
...@@ -71,4 +71,15 @@ class InitDbFixture extends DbFixture ...@@ -71,4 +71,15 @@ class InitDbFixture extends DbFixture
require($file); require($file);
} }
} }
/**
* Toggles the DB integrity check.
* @param boolean $check whether to turn on or off the integrity check.
*/
public function checkIntegrity($check)
{
foreach ($this->schemas as $schema) {
$this->db->createCommand()->checkIntegrity($check, $schema)->execute();
}
}
} }
...@@ -8,37 +8,15 @@ ...@@ -8,37 +8,15 @@
namespace yiiunit\framework\test; namespace yiiunit\framework\test;
use yii\test\ActiveFixture; use yii\test\ActiveFixture;
use yiiunit\data\ar\ActiveRecord;
use yii\test\FixtureTrait; use yii\test\FixtureTrait;
use yii\test\InitDbFixture; use yii\test\InitDbFixture;
use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\Customer;
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\framework\test\Customer'; public $modelClass = 'yiiunit\data\ar\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
...@@ -81,7 +59,7 @@ class ActiveFixtureTest extends DatabaseTestCase ...@@ -81,7 +59,7 @@ class ActiveFixtureTest extends DatabaseTestCase
{ {
parent::setUp(); parent::setUp();
\Yii::$app->setComponent('db', $this->getConnection()); \Yii::$app->setComponent('db', $this->getConnection());
Customer::$db = $this->getConnection(); ActiveRecord::$db = $this->getConnection();
} }
public function tearDown() public function tearDown()
......
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