47

I'm currently working with PHPUnit to try and develop tests alongside what I'm writing, however, I'm currently working on writing the Session Manager, and am having issues doing so...

The constructor for the Session handling class is

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

However, as PHPUnit sends out text before it starts the testing, any testing on this Object returns a failed test, as the HTTP "Headers" have been sent...

kenorb
  • 155,785
  • 88
  • 678
  • 743
Mez
  • 24,430
  • 14
  • 71
  • 93

11 Answers11

39

Well, your session manager is basically broken by design. To be able to test something, it must be possible to isolate it from side effects. Unfortunately, PHP is designed in such a way, that it encourages liberal use of global state (echo, header, exit, session_start etc. etc.).

The best thing you can do, is to isolate the side-effects in a component, that can be swapped at runtime. That way, your tests can use mocked objects, while the live code uses adapters, that have real side-effects. You'll find that this doesn't play well with singletons, which I presume you're using. So you'll have to use some other mechanism for getting shared objects distributed to your code. You can start with a static registry, but there are even better solutions if you don't mind a bit of learning.

If you can't do that, you always have the option of writing integration-tests. Eg. use the PHPUnit's equivalent of WebTestCase.

duckbrain
  • 1,219
  • 1
  • 13
  • 26
troelskn
  • 115,121
  • 27
  • 131
  • 155
  • Thanks, I needed this tough love! My design is unquestioningly better now that I've isolated environmental side effects from the rest of my class. – DaveGauer Mar 05 '14 at 01:10
  • 1
    The documentation for WebTestCase is now at http://www.simpletest.org/en/web_tester_documentation.html. – untill Mar 17 '15 at 12:10
20

Create a bootstrap file for phpunit, which calls:

session_start();

Then start phpunit like this:

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

The bootstrap file gets called before everything else, so the header hasn't been sent and everything should work fine.

Dominik
  • 336
  • 2
  • 4
  • 1
    If you are using an XML configuration file, you can set up the bootstrap by adding a bootstrap attribute to your root `` element like this: `...` – Joe Lencioni Feb 10 '12 at 17:25
19

phpUnit prints output as the tests run thus causing headers_sent() to return true even in your first test.

To overcome this issue for an entire test suite you simply need to use ob_start() in your setup script.

For example, say you have a file named AllTests.php that is the first thing loaded by phpUnit. That script might look like the following:

<?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;
    }
}
Michael
  • 199
  • 1
  • 2
  • 2
    Best solution for testing black box PHP code you can't change. – aercolino Feb 14 '11 at 09:21
  • Note that PHP 3.6 now does this automatically. –  Dec 14 '11 at 19:38
  • 2
    This solution worked for me :D PHPUnit 3.6 is supposed to implement this but didn't work correctly for me. I added ob_start(); into my bootstrap.php file and all my tests with cookies and headers began working correctly again. – Tim Jun 10 '12 at 16:55
14

I had the same issue and I solved it by calling phpunit with --stderr flag just like this:

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

Hope it helps someone!

  • After an hour or two of searching this was the easiest and best solution to allow me to test code that had setcookie() buried inside it. Using @runInSeparateProcess causes other issues whereas this is perfect (for now). – Frug Sep 19 '14 at 19:54
5

I think the "right" solution is to create a very simple class (so simple it doesn't need to be tested) that's a wrapper for PHP's session-related functions, and use it instead of calling session_start(), etc. directly.

In the test pass mock object instead of a real stateful, untestable session class.

private function __construct(SessionWrapper $wrapper)
{
   if (!$wrapper->headers_sent())
   {
      $wrapper->session_start();
      $this->session_id = $wrapper->session_id();
   }
}
Kornel
  • 97,764
  • 37
  • 219
  • 309
1

I'm wondering why nobody have listed XDebug option:

/**
 * @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);
}
user487772
  • 8,800
  • 5
  • 47
  • 72
0

The creation of the bootstrap file, pointed out 4 posts back seems the cleanest way around this.

Often with PHP we are having to maintain, and try to add some kind of engineering discipline to legacy projects that are abysmally put together. We don't have the time (or the authority) to ditch the whole pile of rubbish and start again, so the first anwer by troelskn isn't always possible as a way forward. ( If we could go back to the initial design, then we could ditch PHP altogether and use something more modern, such as ruby or python, rather than help perpetuate this COBOL of the web development world. )

If you are trying to write unit tests for modules that use session_start or setcookie throughout them, than starting the session in a boostrap file gets your round these issues.

0

Can't you use output buffering before starting the test? If you buffer everything that is output, you shouldn't have problems setting any headers as no output would have yet been sent to client at that point.

Even if OB is used somewhere inside your classes, it is stackable and OB shouldn't affect what's going on inside.

pilsetnieks
  • 10,330
  • 12
  • 48
  • 60
  • it seems that ob_start() doesn't actually reset whether PHP thinks the headers are sent. – Mez Oct 10 '08 at 06:19
  • Yeah, ob_start doesnt work with the headers, so that even if you use it, it'll still complain if headers cannot be sent anymore. I could edit up PHPUnit for this though... – Mez Oct 10 '08 at 06:25
0

As far as I know Zend Framework uses the same output buffering for their Zend_Session package tests. You can take a look at their test cases to get you started.

0

It seems that you need to inject the session so that you can test your code. The best option I have used is Aura.Auth for the authentication process and using NullSession and NullSegment for testing.

Aura testing with null sessions

The Aura framework is beautifully written and you can use Aura.Auth on its own without any other Aura framework dependencies.

lost in binary
  • 544
  • 1
  • 4
  • 11
0

As I'm unittesting my bootstrap right now (yes I know most of you don't do that), I'm running in to the same problem (both header() and session_start()). The solution I found is rather simple, in your unittest bootstrap define a constant and simply check it before sending the header or starting the session:

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

// bootstrap.php (application bootstrap)
defined('UNITTEST_RUNNING') || define('UNITTEST_RUNNING', false);
.....
if(UNITTEST_RUNNING===false){
    session_start();
}

I agree that this is not perfect by design, but I'm unittesting an existing application, rewriting large parts is not desired. I also using the same logic to test private methods using the __call() and __set() magic methods.

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