5

I'm running into an issue when trying to run a controller based unit test on a controller method that implements Sessions.

In this case, here is the controller method:

/**
 * @Route("/api/logout")
 */
public function logoutAction()
{
    $session = new Session();
    $session->clear();

    return $this->render('PassportApiBundle:Login:logout.html.twig');
}

And the functional test:

public function testLogout()
{
    $client = static::createClient();
    $crawler = $client->request('GET', '/api/logout');
    $this->assertTrue($client->getResponse()->isSuccessful());
}

The error that is produced:

Failed to start the session because headers have already been sent. (500 Internal Server Error)

I've tried placing in $this->app['session.test'] = true; into the test, but still no go. Has anyone tried resolving an issue like this to unit testing a controller that uses a session?

Wouter J
  • 41,455
  • 15
  • 107
  • 112
Steven Lu
  • 2,150
  • 1
  • 24
  • 33
  • 1
    If you look at the test for the `Session` class (https://github.com/symfony/symfony/blob/2.1/src/Symfony/Component/HttpFoundation/Tests/Session/SessionTest.php), it looks like you should inject an instance of `MockArraySessionStorage` to avoid starting the session. – Sven Dec 03 '12 at 23:37
  • I set those lines up, however, how do I pass it to the `$client = static::createClient();`? Adding those lines alone results to the same error. – Steven Lu Dec 04 '12 at 00:14
  • Yes, the problem is your controller does not offer to inject anything. The `new` operation has no parameters, and there would be no way to pass anything from the test anyways. But I think Symfony 2 should be able to make it's DI framework usable for this. Have you read for example this: http://stackoverflow.com/questions/10106195/symfony-2-dependency-injection-di-of-controllers Additionally, the Symphony docs suggest getting to the Session like this: `$session = $this->getRequest()->getSession();` - this could enable the PHPUnit bootstrapping to use mock session objects – Sven Dec 04 '12 at 00:25

3 Answers3

13

First of all you should use session object from container. So your action should look more like:

/**
 * @Route("/api/logout")
 */
public function logoutAction()
{
    $session = $this->get('session');
    $session->clear();

    return $this->render('PassportApiBundle:Login:logout.html.twig');
}

And then in your test you can inject service into "client's container". So:

public function testLogout()
{
    $sessionMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session')
        ->setMethods(array('clear'))
        ->disableOriginalConstructor()
        ->getMock();

    // example assertion:
    $sessionMock->expects($this->once())
        ->method('clear');

    $client = static::createClient();
    $container = $client->getContainer();
    $container->set('session', $sessionMock);

    $crawler = $client->request('GET', '/api/logout');
    $this->assertTrue($client->getResponse()->isSuccessful());
}

With this code you can do everything you want with your session service. But You have to be aware two things:

  • This mock will be set ONLY for one request (if you want use it in next one, you should set up it again). It's because the client restart kernel and rebuild container between each request.
  • Session handling in Symfony 2.1 is little different than Symfony 2

edit:

I've added an assertion

Cyprian
  • 11,174
  • 1
  • 48
  • 45
  • 1
    Thanks. Changing to `$session = $this->get('session')` made it work perfectly. I don't know why the docs told me to use `new Session()` though... http://symfony.com/doc/master/components/http_foundation/sessions.html – Steven Lu Dec 04 '12 at 16:24
8

In my case, it was enough to set

framework:
    session:
        storage_id: session.storage.mock_file

in the config_test.yml. YMMV, and I don't have that much of an idea what I'm actually doing, but it works for me.

scy
  • 7,132
  • 2
  • 27
  • 35
  • I already have that setting in `config_test.yml` by default, but I got `Fatal error: Call to a member function get() on a non-object` at the line `$session->get('...');` where `$session` is from `$session = $this->container->get('request')->getSession();` – Sithu Jan 28 '16 at 03:46
2

Here just to complete Cyprian's response.

As Sven explains and when looking at the symfony's doc http://symfony.com/doc/2.3/components/http_foundation/session_testing.html, you have to instanciate the mock session with a MockFileSessionStorage object as first constructor argument.

You need to use :

    use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage;

And code should be :

    $sessionMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')
        ->setMethods(array('clear'))
        ->disableOriginalConstructor()
        ->setConstructorArgs(array(new MockFileSessionStorage()))
        ->getMock();
NansP
  • 41
  • 4
  • There might be a better way esp with newer version of Symfony2 but I still need to figure out how - https://coderwall.com/p/newa2q/testing-with-sessions-symfony2 – Christian Mar 08 '17 at 14:08