Php 使用需要发送头的项进行单元测试

Php 使用需要发送头的项进行单元测试,php,unit-testing,phpunit,Php,Unit Testing,Phpunit,我目前正在与PHPUnit合作,尝试与我正在编写的内容一起开发测试,但是,我目前正在编写会话管理器,并且在这样做时遇到了一些问题 会话处理类的构造函数是 private function __construct() { if (!headers_sent()) { session_start(); self::$session_id = session_id(); } } 但是,由于PHPUnit在开始测试之前发送文本,因此此对象上的任何

我目前正在与PHPUnit合作,尝试与我正在编写的内容一起开发测试,但是,我目前正在编写会话管理器,并且在这样做时遇到了一些问题

会话处理类的构造函数是

private function __construct()
{
    if (!headers_sent())
    {
        session_start();
        self::$session_id = session_id();
    }
}

但是,由于PHPUnit在开始测试之前发送文本,因此此对象上的任何测试都会返回失败的测试,因为HTTP“Headers”已被发送…

您不能在开始测试之前使用输出缓冲吗?若您对输出的所有内容都进行缓冲,那个么在设置任何头文件时就不会有问题,因为此时还并没有输出发送到客户机


即使OB在类中的某个地方使用,它也是可堆叠的,OB不应该影响内部的情况。

据我所知,Zend Framework在Zend_会话包测试中使用相同的输出缓冲。您可以查看一下他们的测试用例,开始使用。

好吧,您的会话管理器基本上被设计破坏了。为了能够测试某些东西,必须能够将其从副作用中分离出来。不幸的是,PHP的设计方式鼓励自由地使用全局状态(
echo
标题
退出
会话启动
等)

您可以做的最好的事情是隔离组件中的副作用,这些副作用可以在运行时交换。这样,您的测试可以使用模拟对象,而实时代码使用适配器,这会产生真正的副作用。你会发现这对单例不太好,我想你正在使用它。因此,您必须使用其他一些机制将共享对象分发到您的代码中。您可以从静态注册表开始,但是如果您不介意学习的话,还有更好的解决方案

如果您不能做到这一点,您总是可以选择编写集成测试。例如,使用PHPUnit的等价物。

我认为“正确”的解决方案是创建一个非常简单的类(非常简单,不需要测试),它是PHP会话相关函数的包装器,并使用它而不是直接调用
session_start()
,等等

在测试过程中模拟对象,而不是真正有状态、不稳定的会话类

private function __construct(SessionWrapper $wrapper)
{
   if (!$wrapper->headers_sent())
   {
      $wrapper->session_start();
      $this->session_id = $wrapper->session_id();
   }
}

为phpunit创建引导文件,该文件调用:

session_start();
然后启动phpunit,如下所示:

phpunit --bootstrap pathToBootstrap.php --anotherSwitch /your/test/path/
phpunit --stderr /path/to/your/test

引导文件在调用其他文件之前被调用,因此头文件还没有被发送,所有文件都应该可以正常工作。

创建引导文件,指出4次发回似乎是解决这一问题的最干净的方法

通常,对于PHP,我们必须进行维护,并尝试将某种工程规程添加到非常复杂的遗留项目中。我们没有时间(或权威)扔掉整堆垃圾,重新开始,因此Troleskn的第一个anwer并不总是可行的。 (如果我们可以回到最初的设计,那么我们就可以完全抛弃PHP,使用更现代的东西,比如ruby或python,而不是帮助web开发世界的COBOL永存。)


如果您试图为在整个模块中使用session\u start或setcookie的模块编写单元测试,那么在boostrap文件中启动会话可以解决这些问题。

phpUnit在测试运行时打印输出,从而导致headers\u sent()即使在第一次测试中也返回true

要解决整个测试套件的这个问题,只需在安装脚本中使用ob_start()

例如,假设您有一个名为AllTests.php的文件,这是phpUnit加载的第一个文件。该脚本可能如下所示:

<?php

ob_start();

require_once 'YourFramework/AllTests.php';

class AllTests {
    public static function suite() {
        $suite = new PHPUnit_Framework_TestSuite('YourFramework');
        $suite->addTest(YourFramework_AllTests::suite());
        return $suite;
    }
}

当我现在正在对我的引导进行单元测试时(是的,我知道你们大多数人不会这么做),我遇到了同样的问题(header()和session_start()。
我找到的解决方案相当简单,在unittest引导中定义一个常量,并在发送标头或启动会话之前检查它:

// phpunit_bootstrap.php
define('UNITTEST_RUNNING', true);

// bootstrap.php (application bootstrap)
defined('UNITTEST_RUNNING') || define('UNITTEST_RUNNING', false);
.....
if(UNITTEST_RUNNING===false){
    session_start();
}
我同意这在设计上并不完美,但我正在对现有的应用程序进行单元测试,不需要重写大的部分。我还使用相同的逻辑来测试使用_call()和_set()魔术方法的私有方法

public function __set($name, $value){
    if(UNITTEST_RUNNING===true){
       $name='_' . $name;
       $this->$name=$value;
    }
    throw new Exception('__set() can only be used when unittesting!');
 }

我也遇到了同样的问题,我用--stderr标志调用phpunit解决了这个问题,如下所示:

phpunit --bootstrap pathToBootstrap.php --anotherSwitch /your/test/path/
phpunit --stderr /path/to/your/test

希望它能帮助别人

我想知道为什么没有人列出XDebug选项:

/**
 * @runInSeparateProcess
 * @requires extension xdebug
 */
public function testGivenHeaderIsIncludedIntoResponse()
{
    $customHeaderName = 'foo';
    $customHeaderValue = 'bar';

    // Here execute the code which is supposed to set headers
    // ...

    $expectedHeader = $customHeaderName . ': ' . $customHeaderValue;
    $headers = xdebug_get_headers();

    $this->assertContains($expectedHeader, $headers);
}

似乎需要注入会话,以便测试代码。我使用的最佳选项是Aura.Auth进行身份验证,并使用NullSession和NullSegment进行测试


Aura框架写得很漂亮,您可以自己使用Aura.Auth,而不需要任何其他Aura框架依赖项。

似乎ob_start()实际上不会重置PHP是否认为发送了头。是的,ob_start不处理头,因此即使您使用它,如果头无法再发送,它也会抱怨。我可以为此编辑PHPUnit…测试黑盒PHP代码的最佳解决方案,您不能更改。请注意,PHP3.6现在可以自动执行此操作。此解决方案对我有效:D PHPUnit 3.6应该实现此功能,但对我无效。我添加了ob_start();在我的bootstrap.php文件中,所有带有cookie和标头的测试都开始正常工作。如果您使用的是XML配置文件,您可以通过向根
元素添加bootstrap属性来设置引导,如下所示:
..
我刚刚遇到了相同的问题,但下面的答案都不起作用。你最后做了什么?请给我发邮件。谢谢,我需要这份坚强的爱!我的设计无疑是更好的,因为我已经将环境的副作用从我的课程中分离出来。WebTestCase的文档现在位于。经过一两个小时的搜索,这是最简单、最好的解决方案,可以让我测试嵌入了setcookie()的代码。使用@runinsepareprocess