0

I have the following situation. Let's say I have two classes:

class Session {
    public function start() {
        return session_start();
    }

    // methods for all the other session functions of PHP
}

And a child class which extends Session

class TrustedSession extends Session {
    public function start() {
        if(parent::start() === false)
            return false;

        $requestRemoteAddress = isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:null;
        $requestUserAgent = isset($_SERVER['HTTP_USER_AGENT'])?$_SERVER['HTTP_USER_AGENT']:null;

        $trustedRemoteAddress = $this->get('TRUSTED_REMOTE_ADDR');
        $prevUserAgent = $this->get('PREV_USERAGENT');

        if($trustedRemoteAddress !== $requestRemoteAddress || $prevUserAgent !== $requestUserAgent)
            $this->regenerateID(true);

        return true;
    }
}

So no I want to test my TrustedSession class. But I have to use a mock of Session because according to this question here I can't use the real session functions of PHP because they lead to a Output already started error with PHPUnit.

And now the question: How to mock up the Session class. I mean I can create a SessionInterface which defines the methods I have to implement for Session and the mockup of it. But how can I extend my TrustedSession class with the mockup then?

My idea is to swap parent class at runtime but I think this is not possible (at least in PHP). It's a design question and I'm thinking around the interface I described above but this does not help me out of this situation. Is there a "clean" and nice solution?

Community
  • 1
  • 1
TiMESPLiNTER
  • 5,741
  • 2
  • 28
  • 64
  • Why don't you mock TrustedSession directly?. You can mock methods inherited from the parent class. – gontrollez Jul 17 '14 at 08:04
  • @gontrollez How can I test `TrustedSession` if it's a mock? Probably I can't follow your thoughts. It would be nice if you could show me what you're thinking about in an answer :-) – TiMESPLiNTER Jul 17 '14 at 08:07

2 Answers2

0

Define an interface and make Session a dependency of TrustedSession:

interface ISession
{
    public function start();
}

class Session implements ISession
{
    public function start()
    {
        return session_start();
    }
}

class TrustedSession implements ISession
{
    private $session;

    public function __construct(Session $session)
    {
       $this->session = $session;
    }

    public function start()
    {
        if (!$this->session->start()) {
            return false;
        }

        $requestRemoteAddress = isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:null;
        ....

        return true;

    }
}

This way you can inject a Session Mock when testing TrustedSession

gontrollez
  • 6,372
  • 2
  • 28
  • 36
  • Yep your second thought is what I thought about too. That would be a nice solution but the problem is, this makes no sense because `TrustedSession` must have the exact same methods as `Session`... and it's really an extension to the basic session class because it improves funtionality of `Session`... – TiMESPLiNTER Jul 17 '14 at 09:09
  • Then, you can define an interface that both Session and TrustedSession should implement, and make Session a dependency of TrustedSession. – gontrollez Jul 17 '14 at 09:15
  • I can't think of a good implementation of that scenario. Could you probably mock it up in your answer? – TiMESPLiNTER Jul 17 '14 at 09:24
  • Thx for your update. But the constructor takes now a `Session` instance as paramter. But then I can't exchange this. I think it should take an `ISession` instance shouldn't it? Well here again is the problem that I have to implement all methods even if they are exact the same as the `ISession` implementing class instance of the `TrustedSession` instance. – TiMESPLiNTER Jul 17 '14 at 09:55
  • TrustedSession takes a Session because it needs a Session, not any ISession object. As for the method duplication, there are ways to solve it, e.g using __call – gontrollez Jul 17 '14 at 10:05
  • Okay but how can I exchange the `Session` instance in `TrustedSession` then with a mock if the `Session` class is a specific implmenetation with PHP session function calls? `__call` does not help in this situation because `TrustedSession` implements the `ISession` interface and so it has to implement each and every method defined in `ISession` interface. – TiMESPLiNTER Jul 17 '14 at 10:13
  • A mock is a subclass, so if you mock Session, you get a Session object that you can pass. The __call thing was to avoid rewriting in TrustedSession the methods that don't change from Session. – gontrollez Jul 17 '14 at 10:22
  • Yes, but how can I avoid rewriting the methods in `TrustedSession` with a `__call()` emthod if `TrustedSession` implements the interface `ISession` which forces me to write out each method? – TiMESPLiNTER Jul 17 '14 at 10:25
  • You're right about the __call and the interface. Forget that. – gontrollez Jul 17 '14 at 10:26
-1

I answer the question myself because I got a way to work around this issue with Output already started when using PHPUnit for testing in combination with PHP functions that send headers to the client (like session_start() does).

It's really simple but also very hacky and for sure unclean. What I did is I used the phpunit.xmls option to execute a bootstrap file.

<phpunit bootstrap="bootstrap.php" colors="true">
    <!-- testsuites here -->
</phpunit>

And in the bootstrap.php file I put those lines of code:

ob_start();

// Composer autoload file
require '../vendor/autoload.php';

Now all the output which PHPUnit sends to the client get cached until the end of the script. This works with PHPUnit in version 3.7.*. Didn't test it yet with version 4.x because I'm using PHPStorm for development.

This also only works with the standard unit testing and it fails with code coverage because there is somewhere in the code coverage module a forced "clean and send to the client" for the output buffering which I started in my bootstrap.php file.

TiMESPLiNTER
  • 5,741
  • 2
  • 28
  • 64
  • This answer is unrelated to your question. Nobody could know what do you needed the Session classes for. If you had asked about the "Output already started problem" maybe you could have get a better solution. – gontrollez Jul 28 '14 at 08:24
  • Well the question was clearly about the `Output already started problem` problem (I also linked to a question with this problem) and I looked for a clean solution. As I mentioned I'm not 100% happy with my answer but it is the best I can give to my question. And I thought about swaping the classes as a clean solution but this is no possible so I ended up with `ob_start()` as solution. But yeah the question title is somehow missleading. – TiMESPLiNTER Jul 28 '14 at 09:42