PHPUnit - Unit Testing с элементами, которые должны отправлять заголовки

В настоящее время я работаю с PHPUnit, чтобы попытаться разработать тесты вместе с тем, что я пишу, однако в настоящее время я работаю над написанием диспетчера сеансов, и у меня возникают проблемы с этим...

Конструктор класса обработки сеанса

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

Однако, поскольку PHPUnit отправляет текст перед началом тестирования, любое тестирование этого объекта возвращает неудачный тест, поскольку HTTP-заголовки отправлены...

Ответ 1

Хорошо, ваш менеджер сеансов в основном разбит по дизайну. Чтобы быть в состоянии проверить что-то, должно быть возможно изолировать его от побочных эффектов. К сожалению, PHP разработан таким образом, что он поощряет либеральное использование глобального состояния (echo, header, exit, session_start и т.д. И т.д.).

Лучшее, что вы можете сделать, это изолировать побочные эффекты в компоненте, который можно поменять местами во время выполнения. Таким образом, ваши тесты могут использовать смешные объекты, в то время как в реальном коде используются адаптеры, которые имеют реальные побочные эффекты. Вы обнаружите, что это не очень хорошо сочетается с синглонами, которые, как я полагаю, вы используете. Таким образом, вам придется использовать какой-то другой механизм для распространения общих объектов на ваш код. Вы можете начать со статического реестра, но есть даже лучшие решения, если вы не возражаете немного учиться.

Если вы не можете этого сделать, у вас всегда есть возможность написать интеграционные тесты. Например. используйте эквивалент PHPUnit WebTestCase.

Ответ 2

Создайте загрузочный файл для phpunit, который вызывает:

session_start();

Затем запустите phpunit следующим образом:

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

Файл начальной загрузки вызывается перед всем остальным, поэтому заголовок не отправлен, и все должно работать нормально.

Ответ 3

phpUnit выводит результат при запуске тестов, в результате чего headers_sent() возвращает true даже в вашем первом тесте.

Чтобы решить эту проблему для всего набора тестов, вам просто нужно использовать ob_start() в настройке script.

Например, скажем, у вас есть файл с именем AllTests.php, который является первым, загруженным phpUnit. Этот script может выглядеть следующим образом:

<?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;
    }
}

Ответ 4

У меня была такая же проблема, и я решил ее, вызвав phpunit с флагом -stderr так:

phpunit --stderr /path/to/your/test

Надеюсь, это поможет кому-то!

Ответ 5

Я думаю, что "правильное" решение - создать очень простой класс (настолько простой, что его не нужно тестировать), который является оболочкой для функций, связанных с сеансом PHP, и использовать его вместо вызова session_start() и т.д..

В тестовом прохождении mock-объект вместо реального состояния, неустойчивого класса сеанса.

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

Ответ 6

Мне интересно, почему никто не указал параметр 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);
}

Ответ 7

Не можете ли вы использовать буферизацию вывода перед началом теста? Если вы буферизируете все, что выводится, у вас не должно быть проблем с настройкой каких-либо заголовков, поскольку в этот момент еще не будет отправлено сообщение клиенту.

Даже если OB используется где-то внутри ваших классов, он стекируется, и OB не должен влиять на то, что происходит внутри.

Ответ 8

Насколько я знаю, Zend Framework использует ту же буферизацию вывода для своих тестов пакета Zend_Session. Вы можете взглянуть на свои тестовые примеры, чтобы начать работу.

Ответ 9

Создание файла bootstrap, указывающего на 4 сообщения назад, кажется самым чистым способом.

Часто с PHP нам нужно поддерживать и пытаться добавить какую-то инженерную дисциплину к старым проектам, которые ужасно сведены вместе. У нас нет времени (или авторитета), чтобы вырвать всю груду мусора и начать снова, поэтому первый ответ от troelskn не всегда возможен, как способ продвижения вперед. (Если бы мы могли вернуться к первоначальному дизайну, то мы могли бы полностью протолкнуть PHP и использовать что-то более современное, такое как ruby ​​или python, а не помогать увековечить этот COBOL в мире веб-разработки.)

Если вы пытаетесь написать модульные тесты для модулей, которые используют session_start или setcookie на всем протяжении, чем запуск сеанса в файле boostrap, вы столкнетесь с этими проблемами.

Ответ 10

Как я сейчас начинаю мой загрузочный блок (да, я знаю, что большинство из вас этого не делает), я столкнулся с той же проблемой (как header(), так и session_start()). Решение, которое я нашел, довольно просто, в вашем unittest bootstrap определите константу и просто проверьте ее перед отправкой заголовка или началом сеанса:

// 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!');
 }

Ответ 11

Кажется, вам нужно ввести сеанс, чтобы вы могли проверить свой код. Лучшим вариантом, который я использовал, является Aura.Auth для процесса аутентификации и использования NullSession и NullSegment для тестирования.

Аура с нулевыми сеансами

Структура Aura прекрасно написана, и вы можете использовать Aura.Auth самостоятельно без каких-либо других зависимостей структуры Aura.