Fixtures

Fixtures 是测试中非常重要的一部分。他们的主要目的是建立一个固定/已知的环境状态以确保
测试可重复并且按照预期方式运行。Yii 提供一个简单可用的 Fixure 框架
允许你精确的定义你的 Fixtures 。

Yii 的 Fixture 框架的核心概念称之为 fixture object 。一个 Fixture object 代表
一个测试环境的某个特定方面,它是 [[yii\test\Fixture]] 或者其子类的实例。
比如,你可以使用 UserFixture 来确保用户DB表包含固定的数据。
你在运行一个测试之前加载一个或者多个 fixture object,并在结束后卸载他们。

一个 Fixture 可能依赖于其他的 Fixtures ,通过它的 [[yii\test\Fixture::depends]] 来指定。
当一个 Fixture 被加载前,它依赖的 Fixture 会被自动的加载;同样,当某个 Fixture 被卸载后,
它依赖的 Fixtures 也会被自动的卸载。

定义一个 Fixture

为了定义一个 Fixture,你需要创建一个新的 class 继承自 [[yii\test\Fixture]]
或者 [[yii\test\ActiveFixture]] 。前一个类对于一般用途的 Fixture 比较适合,
而后者则有一些增强功能专用于与数据库和 ActiveRecord 一起协作。

下面的代码定义一个关于 User ActiveRecord 和相关的用户表的 Fixture:

  1. <?php
  2. namespace app\tests\fixtures;
  3. use yii\test\ActiveFixture;
  4. class UserFixture extends ActiveFixture
  5. {
  6. public $modelClass = 'app\models\User';
  7. }

技巧:每个 ActiveFixture 都会准备一个 DB 表用来测试。你可以通过设置 [[yii\test\ActiveFixture::tableName]]
或 [[yii\test\ActiveFixture::modelClass]] 属性来指定具体的表。如果是后者,
表名会从 modleClass 指定的 ActiveRecord 中获取。

Note: [[yii\test\ActiveFixture]] 仅限于 SQL 数据库,对于 NoSQL 数据库,
Yii 提供以下 ActiveFixture 类:

  • Mongo DB: [[yii\mongodb\ActiveFixture]]
  • Elasticsearch: [[yii\elasticsearch\ActiveFixture]] (since version 2.0.2)

提供给 ActiveFixture 的 fixture data 通常放在一个路径为 FixturePath/data/TableName.php 的文件中,
其中 FixturePath 代表 Fixture 类所在的路径,
TableName 则是和 Fixture 关联的表。在以上的例子中,
这个文件应该是 @app/tests/fixtures/data/user.php
data 文件返回一个包含要被插入用户表中的数据文件,比如:

  1. <?php
  2. return [
  3. 'user1' => [
  4. 'username' => 'lmayert',
  5. 'email' => 'strosin.vernice@jerde.com',
  6. 'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
  7. 'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
  8. ],
  9. 'user2' => [
  10. 'username' => 'napoleon69',
  11. 'email' => 'aileen.barton@heaneyschumm.com',
  12. 'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
  13. 'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
  14. ],
  15. ];

你可以给某行指定一个 alias 别名,这样在你以后的测试中,你可以通过别名来确定某行。
在上面的例子中,这两行指定别名为 user1user2

同样,你不需要特别的为自动增长(auto-incremental)的列指定数据,
Yii 将会在 Fixture 被加载时自动的填充正确的列值到这些行中。

技巧:你可以通过设置 [[yii\test\ActiveFixture::dataFile]] 属性来自定义 data 文件的位置。
同样,你可以重写 [[yii\test\ActiveFixture::getData()]] 来提供数据。

如之前所述,一个 Fixture 可以依赖于其他的 Fixture 。比如一个 UserProfileFixture 可能需要依赖于 UserFixture
因为 user profile 表包括一个指向 user 表的外键。那么,
这个依赖关系可以通过 [[yii\test\Fixture::depends]] 属性来指定,比如如下:

  1. namespace app\tests\fixtures;
  2. use yii\test\ActiveFixture;
  3. class UserProfileFixture extends ActiveFixture
  4. {
  5. public $modelClass = 'app\models\UserProfile';
  6. public $depends = ['app\tests\fixtures\UserFixture'];
  7. }

依赖关系确保所有的 Fixtures 能够以正常的顺序被加载和卸载。在以上的例子中,
为确保外键存在, UserFixture 会在 UserProfileFixture 之前加载,
同样,也会在其卸载后同步卸载。

在上面,我们展示了如何定义一个 DB 表的 Fixture 。为了定义一个与 DB 无关的 Fixture
(比如一个fixture关于文件和路径的),你可以从一个更通用的基类 [[yii\test\Fixture]] 继承,
并重写 [[yii\test\Fixture::load()|load()]] 和 [[yii\test\Fixture::unload()|unload()]] 方法。

使用 Fixtures

如果你使用 CodeCeption 作为你的 Yii 代码测试框架,
你需要考虑使用 yii2-codeception 扩展,这个扩展包含内置的机制来支持加载和访问 Fixtures。
如果你使用其他的测试框架,为了达到加载和访问 Fixture 的目的,
你需要在你的测试用例中使用 [[yii\test\FixtureTrait]]。

在以下示例中,我们会展示如何通过 yii2-codeception 写一个 UserProfile 单元来测试某个 class。

在一个继承自 [[yii\codeception\DbTestCase]] 或者 [[yii\codeception\TestCase]] 的单元测试类中,
你可以在 [[yii\test\FixtureTrait::fixtures()|fixtures()]] 方法中声明你希望使用哪个 Fixture。比如:

  1. namespace app\tests\unit\models;
  2. use yii\codeception\DbTestCase;
  3. use app\tests\fixtures\UserProfileFixture;
  4. class UserProfileTest extends DbTestCase
  5. {
  6. public function fixtures()
  7. {
  8. return [
  9. 'profiles' => UserProfileFixture::className(),
  10. ];
  11. }
  12. // ...test methods...
  13. }

在测试用例的每个测试方法运行前 fixtures() 方法列表返回的 Fixture 会被自动的加载,
并在结束后自动的卸载。同样,如前面所述,当一个 Fixture 被加载之前,
所有它依赖的 Fixture 也会被自动的加载。在上面的例子中,因为 UserProfileFixture
依赖于 UserFixtrue,当运行测试类中的任意测试方法时,
两个 Fixture,UserFixtureUserProfileFixture 会被依序加载。

当我们通过 fixtures() 方法指定需要加载的 Fixture 时,我们既可以使用一个类名,
也可以使用一个配置数组。配置数组可以让你自定义加载的 fixture 的属性名。

你同样可以给一个 Fixture 指定一个别名(alias),在上面的例子中,UserProfileFixture 的别名为 profiles
在测试方法中,你可以通过别名来访问一个 Fixture 对象。比如,
$this->profiles 将会返回 UserProfileFixture 对象。

因为 UserProfileFixtureActiveFixture 处继承,
在后面,你可以通过如下的语法形式来访问 Fixture 提供的数据:

  1. // returns the data row aliased as 'user1'
  2. $row = $this->profiles['user1'];
  3. // returns the UserProfile model corresponding to the data row aliased as 'user1'
  4. $profile = $this->profiles('user1');
  5. // traverse every data row in the fixture
  6. foreach ($this->profiles as $row) ...

注: $this->profiles 的类型仍为 UserProfileFixture
以上的例子是通过 PHP 魔术方法来实现的。

定义和使用全局 Fixtures

以上示例的 Fixture 主要用于单个的测试用例,
在许多情况下,你需要使用一些全局的 Fixture 来让所有或者大量的测试用例使用。
[[yii\test\InitDbFixture]] 就是这样的一个例子,它主要做两个事情:

  • 它通过执行在 @app/tests/fixtures/initdb.php 里的脚本来做一些通用的初始化任务;
  • 在加载其他的 DB Fixture 之前禁用数据库完整性校验,同时在其他的 DB Fixture 卸载后启用数据库完整性校验。

全局的 Fixture 和非全局的用法类似,唯一的区别是你在 [[yii\codeception\TestCase::globalFixtures()]] 中声明它,
而非 fixtures() 方法。当一个测试用例加载 Fixture 时,
它首先加载全局的 Fixture,然后才是非全局的。

[[yii\codeception\DbTestCase]] 默认已经在其 globalFixtures() 方法中声明了 InitDbFixture
这意味着如果你想要在每个测试之前执行一些初始化工作,你只需要调整 @app/tests/fixtures/initdb.php 中的代码即可。
你只需要简单的集中精力中开发单个的测试用例和相关的 Fixture。

组织 Fixture 类和相关的数据文件

默认情况下,Fixture 类会在其所在的目录下面的 data 子目录寻找相关的数据文件。
在一些简单的项目中,你可以遵循此范例。对于一些大项目,
您可能经常为同一个 Fixture 类的不同测试而切换不同的数据文件。
在这种情况下,我们推荐你按照一种类似于命名空间
的方式有层次地组织你的数据文件,比如:

  1. # under folder tests\unit\fixtures
  2. data\
  3. components\
  4. fixture_data_file1.php
  5. fixture_data_file2.php
  6. ...
  7. fixture_data_fileN.php
  8. models\
  9. fixture_data_file1.php
  10. fixture_data_file2.php
  11. ...
  12. fixture_data_fileN.php
  13. # and so on

这样,你就可以避免在测试用例之间产生冲突,并根据你的需要使用它们。

Note: 在以上的例子中,Fixture 文件只用于示例目的。在真实的环境下,你需要根据你的 Fixture 类继承的基类来决定它们的命名。
比如,如果你从 [[yii\test\ActiveFixture]] 继承了一个 DB Fixture,
你需要用数据库表名字作为 Fixture 的数据文件名;如果你从 [[yii\mongodb\ActiveFixture]] 继承了一个 MongoDB Fixture,
你需要使用 collection 名作为文件名。

组织 Fixuture 类名的方式同样可以使用前述的层次组织法,但是,为了避免跟数据文件产生冲突,
你需要用 fixtures 作为根目录而非 data

总结

Note: 本节内容正在开发中。

在上面,我们描述了如何定义和使用 Fixture,在下面,我们将总结出一个
标准地运行与 DB 有关的单元测试的规范工作流程:

  1. 使用 yii migrate 工具来让你的测试数据库更新到最新的版本;
  2. 运行一个测试:
    • 加载 Fixture:清空所有的相关表结构,并用 Fixture 数据填充
    • 执行真实的测试用例
    • 卸载 Fixture
  3. 重复步骤2直到所有的测试结束。

以下部分即将被清除

管理 Fixture

注: 本章节正在开发中

todo: 以下部分将会与 test-fixtures.md 合并。

Fixture 是测试中很很重要的一个部分,它们的主要目的是为你提供不同的测试所需要的数据。
因为这些数据,你的测试将会更高效和有用。

Yii 通过 yii fixture 命令行工具来支持 Fixture,这个工具支持:

  • 把 Fixture 加载到不同的存储工具中,比如:RDBMS,NoSQL 等;
  • 以不同的方式卸载 Fixture(通常是清空存储);
  • 自动的生成 Fixutre 并用随机数据填充。

Fixtures 格式

Fixtures 是不同方法和配置的对象, 官方引用documentation
让我们假设有 Fixtures 数据加载:

  1. #Fixtures 数据目录的 users.php 文件,默认 @tests\unit\fixtures\data
  2. return [
  3. [
  4. 'name' => 'Chase',
  5. 'login' => 'lmayert',
  6. 'email' => 'strosin.vernice@jerde.com',
  7. 'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
  8. 'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
  9. ],
  10. [
  11. 'name' => 'Celestine',
  12. 'login' => 'napoleon69',
  13. 'email' => 'aileen.barton@heaneyschumm.com',
  14. 'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
  15. 'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
  16. ],
  17. ];

如果我们使用 Fixture 将数据加载到数据库,那么这些行将被应用于 users 表。 如果我们使用 nosql Fixtures ,例如 mongodb
fixture,那么这些数据将应用于 users mongodb 集合。 为了了解更多实现各种加载策略,访问官网 documentation
上面的 Fixture 案例是由 yii2-faker 扩展自动生成的, 在这里了解更多 section
Fixture 类名不应该是复数形式。

加载 Fixtures

Fixture 类应该以 Fixture 类作为后缀。默认的 Fixtures 能在 tests\unit\fixtures 命名空间下被搜索到,
你可以通过配置和命名行选项来更改这个行为,你可以通过加载或者卸载指定它名字前面的-来排除一些 Fixtures,像 -User

运行如下命令去加载 Fixture:

  1. yii fixture/load <fixture_name>

必需参数 fixture_name 指定一个将被加载数据的 Fixture 名字。 你可以同时加载多个 Fixtures 。
以下是这个命令的正确格式:

  1. // load `User` fixture
  2. yii fixture/load User
  3. // same as above, because default action of "fixture" command is "load"
  4. yii fixture User
  5. // load several fixtures
  6. yii fixture User UserProfile
  7. // load all fixtures
  8. yii fixture/load "*"
  9. // same as above
  10. yii fixture "*"
  11. // load all fixtures except ones
  12. yii fixture "*" -DoNotLoadThisOne
  13. // load fixtures, but search them in different namespace. By default namespace is: tests\unit\fixtures.
  14. yii fixture User --namespace='alias\my\custom\namespace'
  15. // load global fixture `some\name\space\CustomFixture` before other fixtures will be loaded.
  16. // By default this option is set to `InitDbFixture` to disable/enable integrity checks. You can specify several
  17. // global fixtures separated by comma.
  18. yii fixture User --globalFixtures='some\name\space\Custom'

卸载 Fixtures

运行如下命名去卸载 Fixtures:

  1. // unload Users fixture, by default it will clear fixture storage (for example "users" table, or "users" collection if this is mongodb fixture).
  2. yii fixture/unload User
  3. // Unload several fixtures
  4. yii fixture/unload User,UserProfile
  5. // unload all fixtures
  6. yii fixture/unload "*"
  7. // unload all fixtures except ones
  8. yii fixture/unload "*" -DoNotUnloadThisOne

同样的命名选项: namespace, globalFixtures 也可以应用于该命令。

全局命令配置

当命令行选项允许我们配置迁移命令,
有时我们只想配置一次。例如,
你可以按照如下配置迁移目录:

  1. 'controllerMap' => [
  2. 'fixture' => [
  3. 'class' => 'yii\console\controllers\FixtureController',
  4. 'namespace' => 'myalias\some\custom\namespace',
  5. 'globalFixtures' => [
  6. 'some\name\space\Foo',
  7. 'other\name\space\Bar'
  8. ],
  9. ],
  10. ]

自动生成 fixtures

Yii 还可以为你自动生成一些基于一些模板的 Fixtures。 你能够以不同语言格式用不同的数据生成你的 Fixtures。
这些特征由 Faker 库和 yii2-faker 扩展完成。
关注 guide 扩展获取更多的文档。