我又来水文章了。

今天要写的是给php的Phalcon框架搭建单元测试环境碰到的一系列问题。 标题有标题党的嫌疑,并且不同知识背景下,对这些问题可能看法不同。

下面开始整个环境搭建流程:

开发环境Phalcon的版本是3.3,正好网上有3.4的中文文档,直接按着链接的方法把目录和php文件都创建好了。

php并没内置单元测试的类库,所以要写单元测试需要先安装phpunit,现在的框架都是使用composer进行依赖管理,所以很方便,执行以下命令安装:

composer require phpunit/phpunit 

安装完了,切换到指定目录下,也就是文档中的tests目录,直接执行phpunit就可以看到结果了,如果要在其他目录执行需要手动指定配置文件,例如:phpunit -c ./tests/phpunit.xml

正常情况下结果应该是类似这样的:

Time: 434 ms, Memory: 6.00 MB

OK (2 tests, 3 assertions)

然而第一个问题出现了:

Fatal error: Declaration of Phalcon\Test\PHPUnit\UnitTestCase::setUp() must be compatible with PHPUnit\Framework\TestCase::setUp(): void

这个问题很明显是setUp方法与父类不一致,需要添加返回类型声明(PHP7)的新特性。

这个问题经过查询,有两种解决办法,一个是phpunit降级,另一个是自己改代码。

第一种方式:最新安装的phpunit版本是8.5,安装7.0版本就可以了:

composer remove phpunit/phpunit
composer require phpunit/phpunit ^7.0

第二中方式:手动添加返回类型声明,修改文档中的UnitTestCase.php脚本的setUp方法:

public function setUp(): void

然后再执行,还有一样的错误,看代码是UnitTestCase.php继承的PhalconTestCase有一样的问题,这个基类是通过原文这段话介绍安装的:

为了帮助您构建单元测试,我们制作了一些抽象类,您可以使用它们来引导单元测试本身。这些文件存在于Phalcon Incubator中。 您可以通过将Incubator库添加为依赖项来使用它: composer require phalcon/incubator

直接看代码,根据继承关系也找到了另一个setUp方法,也做同样的修改,问题解决。

第二个问题开始了:

再执行phpunit没有报错了。但是没有执行测试用例,输出如下:

Time: 259 ms, Memory: 4.00Mb

No tests executed!

测试用例写的好好的,为什么没执行呢?一点思路也没有直接放狗查,还真的很容易就找到了

采纳答案的描述没看太懂,但是的确解决了我的问题。用./vendor/bin/phpunit代替全局的phpunit

在执行一下,终于看到了绿色(从未像现在这样期待绿色,前提是xml里面开启了colors,哈哈):

Time: 385 ms, Memory: 6.00 MB

OK (1 test, 2 assertions)

对环境没有依赖的测试用例已经可以正常执行了,但是实际工作中通常都会涉及到数据库等外部资源,不再只是简单的计算了,所以需要把环境初始化完整。

跟所有的MVC框架一样,Phalcon也有一个单入口的index.php文件执行http请求,cli程序也一样需要一个单入口文件来初始化基境,所以很容易的想到在单元测试的入口文件处添加依赖。

其实文档里面已经帮我们创建了,就是TestHelper.php,其实UnitTestCase.phpsetUp方法也是可以的,但是因为初始化环境通常得include许多文件,放在setUp里面不太好,所以就准备写在TestHelper.php里。

基于上面的分析,直接复制index.php里面的代码就差不多了,因为目录不一样注意一些常量的赋值即可。

复制过来之后,简单的对一个数据库查询做了单元测试。

然而就出现了第三个问题:

Phalcon\Di\Exception: Service 'modelsManager' wasn't found in the dependency injection container

很纳闷呀,index.php里面的代码都已经复制过来了,并且没有报错说明是正常执行了的,怎么就没有注入呢?

然而我查呀查始终也找不到原因,中间也尝试过手动注入服务进去(如下代码),但是又会产生新的问题(这里就不贴了,只是换了个Service,问题是一样的),而且我想index.php是正常的,一定不会少注册服务,所以肯定是哪块有问题了,暂时也没思路,放弃了。。。

$di->set(
    "modelsManager",
    function() {
        return new ModelsManager();
    }
);

饭后,我想着没啥事再看看吧。

首先看TestHelper.php,代码最下面是这样的:

Di::setDefault($di);

没有看过源码,做的是大概是:di变量是依赖注入的容器,说白了应该是关联数组,setDefault方法类似是一个单例,其他地方直接get出来在这里初始化好的容器。

这样分析的话,TestHelper.php应该是正常的,继续往下分析,到了UnitTestCase.phpsetUp方法:

parent::setUp();

$di = Di::getDefault();

$this->setDi($di);

如前所述,这个方法用如下代码把容器给获取出来,并设置成了属性,单看后两句应该也没啥问题,那看看parent::setUp()

protected function setUpPhalcon()
{
    $this->checkExtension('phalcon');

    // Reset the DI container
    Di::reset();

    // Instantiate a new DI container
    $di = new Di();

    // Set the URL
    $di->set(
        'url',
        function () {
            $url = new Url();
            $url->setBaseUri('/');

            return $url;
        }
    );

    $di->set(
        'escaper',
        function () {
            return new Escaper();
        }
    );

    $this->di = $di;
}

上面代码,是parent::setUp()最终调用的代码,可以猜测是官方把我们之前setDefault的容器给reset掉了,哭笑不得ing

鉴于官方这段代码也没做什么事,简单粗暴的处理办法是直接注释掉,UnitTestCase.phpsetUp方法只保留下面这段代码:

$di = Di::getDefault();

$this->setDi($di);

$this->_loaded = true;

应该正常了吧,然而还是有问题:

TypeError: Argument 1 passed to Phalcon\Test\PHPUnit\UnitTestCase::setDI() must implement interface Phalcon\DiInterface, null given, called in xxx

这个为题是因为$this->setDi($di);,源代码中被跳过的部分可以看到$di = new Di();,这里的di是Di类的实例,而现在报错的di是FactoryDefault类的实例

解决报错的方便简单粗暴可以把官方代码的参数类型声明去掉,也即:

public function setDI(DiInterface $di)

改为:

public function setDI($di)

这样就可以愉快的玩耍了,简直累死个人。

终于可以正常运行了

结束语

Phalcon这个框架相对来说还是小众了一点,国内的资料很少。但是官方的交流群还是不错的,虽然这几个问题没有一个是在官方找到答案的。

写完文章脖子都疼了,又是没啥技术含量的水文,哈哈,睡觉😴

(完)