使用PHPUnit测试PHP头信息

110

我正在尝试使用PHPunit测试一个输出自定义头部的类。

问题是在我的机器上,以下代码:

<?php

class HeadersTest extends PHPUnit_Framework_TestCase {

    public function testHeaders()
    {
        ob_start();

        header('Location: foo');
        $headers_list = headers_list();
        header_remove();

        ob_clean();

        $this->assertContains('Location: foo', $headers_list);
    }
}

或甚至是这样:

<?php

class HeadersTest extends PHPUnit_Framework_TestCase {

    public function testHeaders()
    {
        ob_start();

        header('Location: foo');
        header_remove();

        ob_clean();
    }
}

返回此错误:

name@host [~/test]# phpunit --verbose HeadersTest.php 
PHPUnit 3.6.10 by Sebastian Bergmann.

E

Time: 0 seconds, Memory: 2.25Mb

There was 1 error:

1) HeadersTest::testHeaders
Cannot modify header information - headers already sent by (output started at /usr/local/lib/php/PHPUnit/Util/Printer.php:173)

/test/HeadersTest.php:9

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

看起来好像有其他内容在测试运行之前输出到终端,尽管没有包含其他文件且PHP标签之前也没有其他字符。可能是PHPUnit内部的某些东西引起的吗?

问题可能是什么?


18
如果有其他人对此感兴趣,我想涵盖这一点。在运行PHPunit(使用PHP CLI)时,headers_list()无法工作,但可以使用xdebug_get_headers()代替。 - titel
8个回答

136
问题在于PHPUnit会向屏幕打印标题,此时无法添加更多的标题。解决方法是在隔离的进程中运行测试。以下是一个示例。
<?php

class FooTest extends PHPUnit_Framework_TestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testBar()
    {
        header('Location : http://foo.com');
    }
}

这将导致:

$ phpunit FooTest.php
PHPUnit 3.6.10 by Sebastian Bergmann.

.

Time: 1 second, Memory: 9.00Mb

OK (1 test, 0 assertions)

@runInSeparateProcess注解很关键。

如果你使用的是PHPUnit ~4.1或其他版本,并且遇到了错误:

PHP Fatal error:  Uncaught Error: Class 'PHPUnit_Util_Configuration' not found in -:378
Stack trace:
#0 {main}
  thrown in - on line 378

Fatal error: Uncaught Error: Class 'PHPUnit_Util_Configuration' not found in - on line 378

Error: Class 'PHPUnit_Util_Configuration' not found in - on line 378

Call Stack:
    0.0013     582512   1. {main}() -:0
尝试将以下内容添加到您的bootstrap文件中以修复它:
<?php
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
    define('PHPUNIT_COMPOSER_INSTALL', __DIR__ . '/path/to/composer/vendors/dir/autoload.php');
}

7
这会导致在存在一些define()语句时出现错误。 PHPUnit_Framework_Exception: 注意:常量xyz已经定义。 - Minhaz
1
@mebjas 这听起来不相关。 - SamHennessy
4
应用程序出现了这个错误:PHP致命错误:在phar:///usr/local/bin/phpunit/phpunit/Util/GlobalState.php:211处抛出异常'Exception',异常消息为'SplFileInfo的序列化不被允许' - t1gor
2
这绝对是解决问题的最佳选择。它像魔法一样奏效! - xarlymg89
2
我不得不使用xdebug_get_headers()函数来获取设置头的数组。headers_list()全局函数在我的情况下无法工作。 - Shalom Sam
显示剩余8条评论

111

虽然在单独的进程中运行测试确实可以解决问题,但在运行大量测试套件时会有明显的开销。

我的解决方法是将phpunit的输出重定向到stderr,代码如下:

phpunit --stderr <options>

这应该可以解决问题,而且也意味着您不必创建包装函数并替换代码中的所有出现。


3
太棒了!我的代码没有任何改动,也不需要单独的进程,它就能正常运行。 - alexfernandez
但是我依然能看到我所犯的错误并且得到 error_reporting(E_ALL) 的输出吗? - spankmaster79
1
@spankmaster79 是的,它只会进入您的终端标准错误。默认情况下,大多数终端将打印标准输出和标准错误在一起,但它们实际上是分开的流。 - Jon Cairns
我快被搞疯了!谢谢这个技巧,它确实有意义,错误应该报告给标准错误流!谢谢。 - th3n3rd
45
在你的phpunit.xml文件中添加stderr="true"可以节省一些按键操作。 - tszming
我知道这是一个旧帖子,但我在这个解决方案中遇到的问题是它不能从我的单元测试的phpunit.xml生成junit.xml文件。我猜测这是因为它将输出发送到stderr而不是stdout。 - xCHAN

18

附带说明:对于我来说,headers_list() 一直返回0个元素。 我注意到@titel在问题上的评论,认为它在这里值得特别提一下:

只是想在这里提及一下,如果有其他人也感兴趣的话。在运行 PHPunit(使用 PHP CLI)时,headers_list() 不起作用,但可以使用 xdebug_get_headers() 代替。

HTH


7

如已在评论中提到的那样,我认为更好的解决方案是在XML配置文件中定义processIsolation,如下所示:

     <?xml version="1.0" encoding="UTF-8"?>
     <phpunit
        processIsolation            = "true"
        // ... 
     >
     </phpunit>

这样做,您就不必传递--stderr选项,这可能会让您的同事感到恼火。


4
最好只为需要它的测试定义它。对所有测试进行设置只会使运行测试变慢。 - Shi
@Shi - 你是怎么做到的?在 <testsuite><file> 元素上不允许使用 processIsolation 属性。 - Richard Smith

3
我有一个更激进的解决方案,可以在我的测试/包含文件中使用$_SESSION。 我编辑了一个PHPUnit文件,在../PHPUnit/Utils/Printer.php中加入了"session_start();",然后在命令"print $buffer"之前执行。 对我来说,它非常有效。但我认为用户"joonty"的解决方案是迄今为止最好的。

你在代码库中提交了 pull-request 吗?我猜这可能是一个常见的问题。 - t1gor
谢谢提示,我在我的phpunit bootstrap.php中调用了session_start(),现在它可以正常工作了。 - bumperbox

1

在执行PHPUnit测试后,使用--stderr参数获取头信息。

phpunit --stderr

1
如果您正在使用Laravel并在路由文件中添加一些标题,则需要用headers_sent将其包围以在测试期间忽略。以下是一个示例:
if (!headers_sent()) {
    header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: Origin, Access-Control-Allow-Origin,  Content-Type, X-Authorization, Authorization, Accept,charset,boundary,Content-Length');
    header('Access-Control-Allow-Origin: *');
}

然后再次尝试单元测试,它将会通过。

我正在开发一个基于Laravel 5.4的应用程序,并注意到在“./bootstrap/app.php”中存在相同的头文件引用。尝试了你提供的解决方案,它可以正常工作!不需要--stderr或@runInSeparateProcess标志。 - Dale Ryan

0

针对 @runInSeparateProcess 的另一种解决方案是在运行 PHPUnit 时指定 --process-isolation 选项:

name@host [~/test]# phpunit --process-isolation HeadersTest.php

这类似于在phpunit.xml中设置processIsolation="true"选项。
这个解决方案和指定--stderr选项具有类似的优缺点,但在我的情况下没有起作用。基本上不需要进行任何代码更改,尽管由于在单独的PHP进程中运行每个测试,可能会导致性能下降。

这可能是追踪问题并进行一些测试的第一步,但为了创建可维护的测试,最好直接将注释嵌入到测试文件中。需要从命令行中使用的“特殊”选项越少,CI系统配置维护就越容易。 - Shi
谁曾降级 #失败。这个答案作为另一个选项是正确的。 - f b

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接