How to unit test function with rereoute() that calls "die"

122 views
Skip to first unread message

DB

unread,
Dec 18, 2016, 12:11:43 PM12/18/16
to f3-fra...@googlegroups.com
Hello,

I'm trying to write a unit test using F3's mock() and PHPUnit.  I have a function that I want to test that looks like this:

public function logout($f3)
{
    $f3
->clear('SESSION.user');
    $f3
->reroute('@loginPage');
}

I'm trying to test it doing something like:

public function testLogout()
{
    $this
->f3->QUIET = true;
    $this
->f3->HALT = false;
    $this
->f3->mock('GET /logout');

   
//Make sure SESSION.user is cleared
   
//Make sure user gets rerouted
}


The problem is that when reroute() is called, reroute() will call "die" which will terminate the execution of my test suite.  Here is a code snippet from the end of the reroute() function in base.php where "die" gets called.

if (!$this->hive['CLI']) {
    header
('Location: '.$url);
    $this
->status($permanent?301:302);
    die;
}
$this->mock('GET '.$url);

I did see in some places (even in F3 too, see snippet above) that you can call mock() instead.  I was able to get this to work and test my function successfully if I change my logout() function to be:

public function logout($f3)
{
    $f3
->clear('SESSION.user');
    $f3
->mock('GET @loginPage');
}

However, this feels like cheating to me.  mock() is meant for testing, not production code--unless F3 has a unique way to use mock().  I don't like changing production code to accommodate my tests unless my testing discovers an actual problem.  Is there some better way to test that when I call this function that the user gets redirected as expected?  Is there a different way to reroute someone?  Is mock() really the right way to redirect and not have "die" get called?  

I have opinions about the use of "die" and "exit" in PHP code and I think they should only get called in extreme circumstances.  What role does "die" play in the reroute() function, is it because it sends a 301 or 302 status instead of 200?  Is it possible to reroute someone with a 200 status?

Thanks!

ikkez

unread,
Dec 18, 2016, 1:31:16 PM12/18/16
to Fat-Free Framework
you can use ONREROUTE for this purpose: https://fatfreeframework.com/3.6/quick-reference#ONREROUTE

just create a custom reroute function and return FALSE in order to skip the actual redirection and do unit tests instead ;)

DB

unread,
Dec 18, 2016, 1:48:47 PM12/18/16
to f3-fra...@googlegroups.com
Thanks for the reply.  Some clarification...

When you say to create a custom reroute() function, do you mean to copy-and-paste the existing reroute function and add it in my test code and change "die" to "return false"?  Then call it like so:

$this->f3->set('ONREROUTE', function($url, $permanent = false) {
    $this
->reroute($url);
});

What are your thoughts on doing it this way, without reimplementing reroute():

$this->f3->set('ONREROUTE', function($url, $permanent = false) {
    $this
->f3->mock('GET '. $url);
});

I would prefer not to reimplement reroute() since I think it would add noise to my tests and whenever the reroute() function changes, I would have to be aware of this and remember to update my tests as well.

ikkez

unread,
Dec 19, 2016, 5:34:06 AM12/19/16
to Fat-Free Framework
no I mean you just add a custom ONREROUTE handler to your test case, so you can intercept the rerouting, which breaks your testability.
Something like this:

public function testLogout()
{
    $onreroute_bak
= $this->f3->get('ONREROUTE');


    $this
->f3->set('ONREROUTE', function($url, $permanent = false) {

       
return false; // skip actual reroute
   
});

   
    $this
->f3->QUIET = true;
    $this
->f3->HALT = false;
    $this
->f3->mock('GET /logout');

   
//Make sure SESSION.user is cleared
   
//Make sure user gets rerouted

   
    $this
->f3->set('ONREROUTE',$onreroute_bak);
}


more insights here: https://github.com/bcosca/fatfree/pull/661

DB

unread,
Dec 19, 2016, 10:52:43 PM12/19/16
to Fat-Free Framework
Hmm, something weird is still going on.  If I just return false in the ONREROUTE callback, the RESPONSE variable in the hive is empty so I can't check the HTML response for text, my test fails.  The only way I've gotten my test to work 100% is by doing the following:

public function testLogout()
{
    $this->f3->QUIET = false; //<-- I wish this was false
    $this->f3->HALT = false;
    $this->f3->set('SESSION.user', $this->faker->randomNumber);
    $this->f3->set('ONREROUTE', function($url, $permanent) { //<-- This isn't setup to return false like suggested
        $this->f3->mock('GET '. $url);
    });

    $this->f3->mock('GET /logout');

    $response = $this->f3->RESPONSE; // <-- $response will be set to '' which will cause my test to fail

    $this->assertContains('<title>Login</title>', $response);
    $this->assertEquals($this->f3->get('SESSION.user'), null);

    $this->f3->set('ONREROUTE', $onreroute_bak);
}

If I use the above test, but change "QUIET" to true and/or change "ONREROUTE" to return false my test will fail.  I would like QUIET to be true so that it doesn't print all the HTML of my index page to my terminal when I run my test suite.  Do you know what's going on here?

Here is my logout function that I'm testing if it helps:

public function logout($f3)
{
    $f3
->clear('SESSION.user');
    $f3
->reroute('@loginPage');
}

My routes looks like:

GET @loginPage: / = web\controllers\LoginController->index
GET /
login = web\controllers\LoginController->login
GET
@logout: /logout = web\controllers\LoginController->logout

And the @loginPage (which is really just my index page) looks like:

public function index()
{
    $template
= new \Template;
    echo $template
->render('index.html');
}

Thanks again for the help!

ikkez

unread,
Dec 20, 2016, 6:49:44 AM12/20/16
to Fat-Free Framework
When looking at the core, the response should also be available as return value, like:

 $response = $this->f3->mock('GET /logout');

DB

unread,
Dec 20, 2016, 8:16:31 AM12/20/16
to f3-fra...@googlegroups.com
That's what I thought too; however, in the code snippet you posted $response is always null for me, even in my other test cases.  In other test cases, I can get the response value by getting the RESPONSE variable out of the hive.  But messing with the ONREROUTE and setting QUIET to true seems to break the ability to get a value for RESPONSE.

Here is another test in the same class that passes for me which is why I think something suspicious is going on with the ONREROUTE variable.  Notice that in this test case (unlike the one we're working on) I can set QUIET to true and have it work:

public function testIndex()
{

    $this
->f3->QUIET = true;


    $this
->f3->mock('GET /');

       
    $response
= $this->f3->RESPONSE;


    $this
->assertContains('<title>Login</title>', $response);
}

ikkez

unread,
Dec 20, 2016, 9:20:57 AM12/20/16
to f3-fra...@googlegroups.com
there is no magic around the ONREROUTE handler. please have a look in the base.php core file, maybe you can find what's expected then.

I think maybe it's because you actually don't want the response of the logout page, but the response of the page that is redirected to after logout.
So instead of testing the login page, test for the reroute header or just the url

$reroute_url = '';
$this->f3->set('ONREROUTE', function($url, $permanent) use ($reroute_url) {
  $reroute_url = $url;
  return false;
});
$this->f3->mock('GET /logout')
;
$this
->assertContains('login.html', $reroute_url);

DB

unread,
Dec 20, 2016, 9:39:40 PM12/20/16
to Fat-Free Framework
This still isn't working.  I tried your code example above exactly.  The "die" command near the end of reroute() is still getting executed.  Setting the ONREROUTE seems to have no effect that I can tell in my test.

Looking in base.php the code that seems to be causing me issues is the following in reroute():

if (($handler=$this->hive['ONREROUTE']) &&
    $this
->call($handler,[$url,$permanent])!==FALSE)
   
return;

My code doesn't enter the body of the above if-statement because $this->call($handler,[$url,$permanent]) is false (I'm not 100% sure what that code is doing).  Since false !== false it skips the following "return" command, leading it to hit "die" a few lines later.  The reason it's false is because of the following code in the call() function in base.php

// Execute callback
$out
=call_user_func_array($func,$args?:[]);
if ($out===FALSE)
   
return FALSE;

When my breakpoint is on $out=call_user_func_array($func,$args?:[]); if I step forward once, it takes me into my logout() function which I would expect.  A little later, I get to the body of the closure we made where we told it to return false in the test, so that code is getting executed too.

Thanks for your continued support, it's much appreciated!  I'm looking forward to getting to the bottom of this.

ikkez

unread,
Dec 21, 2016, 5:54:03 AM12/21/16
to Fat-Free Framework
oh i was under the assumption that when returning false, it'll skip the die. so the bottomline is that you should probably just return TRUE in the ONREROUTE handler ;)

DB

unread,
Dec 21, 2016, 6:52:59 PM12/21/16
to Fat-Free Framework
Changing it to "return true" avoids "die" being called; however, the tests still won't pass.  Here is my current implementation:

public function testLogout()
{
    $onrerouteSave
= $this->f3->get('ONREROUTE');


    $this
->f3->set('SESSION.user', $this->faker->randomNumber);


    $rerouteUrl
= '';

    $this
->f3->set('ONREROUTE', function($url, $permanent) use ($rerouteUrl) {
        $rerouteUrl
= $url;
       
return true;
   
});

    $response
= $this->f3->mock('GET /logout');

    $this
->assertNotNull($response); //<-- $response is empty
    $this
->assertEquals('/', $rerouteUrl); //<-- $rerouteUrl is empty

    $this
->assertEquals($this->f3->get('SESSION.user'), null);


    $this
->f3->set('ONREROUTE', $onrerouteSave);
}

I added comments to my code showing why my tests are failing.  I don't know why the $rerouteUrl is empty, the ONREOUTE closure is getting called and I see "$rerouteUrl = $url;" get executed.  I guess it's a scope thing, but I'm not sure why $rerouteUrl is empty once control returns back to my test method.  I was stepping around in base.php a bit and noticed that in run() the variable $body is empty and never gets set with any useful value, it's always being set equal to an empty value, but I don't know why.

The requirements for my test are:
  1. Does it redirect to the path/page I expect?
  2. Does that page contain certain content I'm looking for?
  3. Does the state of the SESSION.user variable change?
I would figure this would be a common problem, but I can't find many people unit testing their F3 applications.

xfra35

unread,
Dec 22, 2016, 5:33:43 PM12/22/16
to Fat-Free Framework
Passing $rerouteUrl as a reference should fix your issue:
$this->f3->set('ONREROUTE', function($url, $permanent) use (&$rerouteUrl) {
    $rerouteUrl
= $url;
});


See the framework tests for an example of usage of this hook.

Drew Budwin

unread,
Dec 22, 2016, 5:39:19 PM12/22/16
to f3-fra...@googlegroups.com, f3-framework+APn2wQdllyYRCbrJQTu...@googlegroups.com
You seem to be onto something xfra35!  My assert that checks that I go to the correct route is working.  The only remaining issue is getting the HTML of the page it rerouted to.  I'm getting an empty value for $response when I tried to do either of the following, any ideas?

$response = $this->f3->mock('GET /logout');

$response
= $this->f3->RESPONSE;

Can you explain why the reference makes a difference here?

Thanks!

xfra35

unread,
Dec 26, 2016, 5:47:19 AM12/26/16
to Fat-Free Framework
The empty response is expected. The GET /logout route does just one thing: it returns a 302 header. There's no output.
Reply all
Reply to author
Forward
0 new messages