Yii2单元测试初探

tests目录结构解析,怎么这么多yml和_bootstrap?codeception运行流程,build干了什么?run干了什么?codeception.yml怎样发挥做用?modules如何被加载?$tester->haveFixtures()方法是哪里来的?php

1.环境bootstrap

Yii2 advanced模板,version:2.0.11.2,已安装codeception扩展,yii2-codeception扩展yii2

2.tests目录结构app

这一版本的tests目录结构不一样于后来的更高版本,但整体思想理解后便于版本升级后的快速理解。frontend

tests
--codeception
----backend
--------unit                  backend单元测试文件目录
------------_bootstrap.php    backend单元测试所需变量定义,执行run->Codecept::run()->runSuite()->SuitManager::initialize()触发SUITE_INIT事件,此时加载这里的_bootstrap文件,注意:此处文件名称应与yml文件中指定的settings:bootstrap一致,不然抛异常
------------TestCase.php      继承自yii\codeception\TestCase,指定了配置文件为'@tests/codeception/config/backend/unit.php'
------------DbTestCase.php    继承自yii\codeception\DbTestCase,指定了配置文件为'@tests/codeception/config/backend/unit.php'
--------acceptance            backend验收测试文件目录
--------functional            backend功能测试文件目录
--------_bootstrap.php        执行build命令时若codeception.yml文件指定了settings:bootstrap则在此时加载其内容,参见Codeception\Configuration::config()
--------codeception.yml       backend全部测试的测试配置信息
--------unit.suite.yml        backend全部单元测试套件的测试配置信息
--------acceptance.suite.yml  backend全部验收测试套件的测试配置信息
--------functional.suite.yml  backend全部功能测试套件的测试配置信息
----config
--------backend
------------unit.php          backend单元测试指定的配置文件
------------acceptance.php    backend验收测试指定的配置文件
------------functional.php    backend功能测试指定的配置文件
------------config.php        backend全部测试均需设置的配置信息
--------frontend
------------配置结构同backend
--------acceptance.php        面向全部验收测试的配置信息
--------functional.php        面向全部功能测试的配置信息
--------unit.php              面向全部单元测试的配置信息
--------config.php            面向全部测试的公共配置信息
--codeception.yml  执行全部测试的测试配置信息

3.运行单元测试yii

(1)cd 进入项目根目录;
(2)执行build命令,-c指定测试配置文件函数

php codeception/codeception/codecept build -c /tests/codeception/backend/codeception.yml单元测试

(3)指定测试yml路径,指定执行某个单元测试
php codeception/codeception/codecept run -c /tests/codeception/backend/codeception.yml -- unit resource/MyTest.php测试

窥探总体运行流程ui

(1)入口:codeception/codeception/Codecept,文件路径vendor/codeception/codeception/codecept,内容实际上是php代码,实例化一个Codeception\Application对象$app,并添加了预设的命令,例如:build,run,GenerateCept等,$app->run();

(2)上面$app->run()得到命令行输入的参数后执行基类Symfony\Component\Console\Application->run() =>doRun()=>doRunCommand();

这一步从参数得到要执行的命令,接下来执行命令。

(3)codeception的全部命令都在vendor/codeception/src/Codeception/Command目录中,均继承自Symfony\Component\Console\Command\Command。以build命令为例:

Symfony\Component\Console\Command\Command::run()=>Codeception\Command\Build::execute()

这里开始了build命令的真正执行,找到了命令的入口,接下来就看看经常使用的build和run命令具体干了些什么,那些配置文件是在什么时候发挥做用的。

4.build干了什么?

(1)命令入口:Codeception\Command\Build::execute()=>$this->buildActorsForConfig()

(2)加载全局测试配置信息: Codeception\Command\Shared\Config::getGlobalConfig() => Codeception\Configuration::config()

这里执行了一个trait的方法用于加载全局测试配置信息,也就是-c参数指定的codeception.yml文件的内容,过程当中会调用Codeception\Configuration::loadBootstrap()此时加载指定测试目录下的bootstrap文件,一般是_bootstrap.php,在3小节中会加载tests/backend/_bootstrap.php文件内容。

(3)构建测试套件:$this->buildSuiteActors(),这里主要看两步骤内容:

1)构建测试方法:在_support/_generated目录下生成与suite的class_name对应的trait文件,内容是在yml中配置的module的全部能够在测试文件中使用的actions

=>$this->buildActions()=>Codeception\Lib\Generator\Actions::produce()

2)构建测试角色:在_support目录下生成与suite的class_name对应的文件,内容是一个actor类,引用了上一步生成的对应suite的action trait
=>$this->buildActor()=>Codeception\Lib\Generator\Actor::produce()

到这里,找到了codeception.yml的加载时机,知道了_support里的文件是怎么来的,以及yml中指定的modules内容是如何能够在测试文件中经过actor对象直接访问的,终于知道模板示例中的$tester->haveFixture()方法是怎么来的了。

5.run命令干了什么?

(1)命令入口:Codeception\Command\Run::execute();=> new Codeception\Codecept();

(2)准备运行测试套件: Codeception\Codecept::run($suite,$test); => Codeception\Codecept::runSuite();

(3)由套件管理器运行测试套件: $suiteManager = new Codeception\SuiteManager(); $suiteManager->initialize()

这里的套件管理器初始化时会触发Events::MODULE_INIT事件,从而执行yml文件指定的modules的_initialize()方法,Events::SUITE_INIT事件,这一事件的订阅者会加载对应suite下的bootstrap文件,在3小节示例中对应的是tests/backend/unit/_bootstrap.php文件。

(4)运行测试:PHPUnit\Runner::doEnhanceRun();

=> yii\codeception\TestCase::run(); => \PHPUnit_Framework_TestCase::run(); => \PHPUnit_Framework_TestResult::run(); startTest(); endTest();

到这里,找到了各个套件下_bootstrap.php文件的加载时机,也找到了modules的初始化时机,这里提到了codeception的事件处理,下面总结下codeception的事件发布订阅机制。

6.dispatcher和subscriber

发布者:Symfony\Component\EventDispatcher\EventDispatcher 实现了EventDispatcherInterface

订阅者:codeception实现的订阅者在目录vendor/codeception/codeception/src/Codeception/Subscriber

第5小节提到的两个订阅者分别是Bootstrap和Module,订阅者都有一个静态成员$events记录着各自订阅的事件与对应的处理方法。

7.modules是怎样运做的

codeception的modules存在于目录:vendor/codeception/codeception/src/Codeception/Module

在build命令执行过程当中须要加载测试依赖的modules并实例化,modules之间能够存在依赖关系,这里离不开依赖注入的实现Codeception\Lib\Di类。

 build命令执行到Codeception\Lib\Generator\Actions::produce()前,在Codeception\Lib\Generator\Actions::__construct()这一步实例化Di,ModuleContainer,获取所需modules;
(1)获取yml配置的modules名称列表: Codeception\Configuration::modules()
(2)使用Di实例化modules并获取可用actions:Codeception\Lib\ModuleContainer::__construct(Di $di,$config) =>Codeception\Lib\ModuleContainer::create()

到这里知道了yml中配置的modules是如何被找到并实例化,以及依赖关系是怎样处理的,那一个具体的module是如何在测试文件中发挥做用的?

以Yii2这个module为例,这里有一些钩子函数,_initialize(),_before()等,分别在测试的不一样环节被执行,就像run命令运行到套件管理器初始化时会触发事件致使module的_initialize()方法得以执行。_before()方法在Events::TEST_BEFORE事件触发时运行,并会根据$configFile指定的配置文件实例化一个Yii::$app对象供测试方法使用。

8.$this->tester属性是在哪里定义?什么时候实例化的?

猜测应该是在Codeception\Test\Unit基类里,毫不会跑到PhpUnit层。上面已经知道Actor是在build命令中构建的,这里的$tester就是Actor的实例。果真在Codeception\Test\Unit类的setUp()方法中找到了该属性的注入代码。

protected function setUp()
    {
       //......此处省略部分代码

        /** @var $di Di  **/
        $di = $this->getMetadata()->getService('di');
        $di->set(new Scenario($this));

        // auto-inject $tester property
        if (($this->getMetadata()->getCurrent('actor')) && ($property = lcfirst(Configuration::config()['actor_suffix']))) {
            $this->$property = $di->instantiate($this->getMetadata()->getCurrent('actor'));
        }

        //......此处省略部分代码
    }