Search code examples
cakephpphpunitcakephp-3.x

Cakephp 3 - behavior not loaded during controller unit test


I have a behavior that I add to all models by initialize event listener.

// src/Event/InitializeEventListener.php 

...

class InitializeEventListener implements EventListenerInterface
{

    public function implementedEvents()
    {
        return [
            'Model.initialize' => 'initializeEvent'
        ];
    }

    public function initializeEvent(Event $event, $data = [], $options = [])
    {
        $event->subject()->addBehavior('MyBehavior');
    }

}


// bootstrap.php

$ieListener = new InitializeEventListener();
EventManager::instance()->on($ieListener);

I have UsersController and index action. If I put debug($this->Users->behaviors()->loaded());die; I can see the default Timestamp and the loaded MyBehavior. So far all working fine and I can use MyBehavior's function in index action. This is when opening the page in the browser.

Now, I have this test function

// tests/TestCase/Controller/UsersControllerTest.php
public function testIndex()
{
    $this->get('/users/index');
    $this->assertResponseSuccess();
}

However, when running the test the MyBehavior is not being loaded(it is NOT in the loaded behavior list, and hence tyring to use it's function gives unknown method error). I tried adding this into the UsersController's testcase

public function setUp()
{
    parent::setUp();
    $this->Users = TableRegistry::get('Users');
    $eventList = new EventList();
    $eventList->add(new Event('Users.initializeEvent'));
    $this->Users->eventManager()->setEventList($eventList);
}

but again MyBehavior is not loaded.

Thanks


Solution

  • A new global event manager per test

    The problem is that the CakePHP test case set a new global event manager instance on setup:

    public function setUp()
    {
        // ...
        EventManager::instance(new EventManager());
    }
    

    https://github.com/cakephp/cakephp/blob/3.2.12/src/TestSuite/TestCase.php#L106

    So everything added to the global event manager before that point is going to be lost.

    This is being done in order to avoid state. If it wouldn't be done, then possible listeners added to the global manager by your tested code in test method A, would still be present in test method B, which could of course cause problems for the code tested there, like listeners stacking up, causing them to be invoked multiple times, listeners being invoked that normally wouldn't be invoked, etc.

    Use Application::bootstrap() in newer CakePHP Versions

    As of CakePHP 3.3 you can use the bootstrap() method in your application's Application class, unlike the config/bootstrap.php file, which is only being included once, that method is being invoked for every integration test request.

    This way your global events are being added after the test case assigns a new clean event manager instance.

    Add global listeners afterwards as a workaround

    In version before CakePHP 3.3, whenever I need gloabl events, I store them in a configuration value and apply them at setup time in the test cases, something along the lines of

    boostrap

    $globalListeners = [
        new SomeListener(),
        new AnotherListener(),
    ];
    Configure::write('App.globalListeners', $globalListeners);
    foreach ($globalListeners as $listener) {
        EventManager::instance()->on($listener);
    }
    

    test case base class

    public function setUp()
    {
        parent::setUp();
    
        foreach (Configure::read('App.globalListeners') as $listener) {
            EventManager::instance()->on($listener);
        }
    }
    

    Your snippet doesn't add any listeners

    That being said, the problem with your setup code snippet is that

    1. It has nothing to with listening to events, but with tracking dispatched events. Also there probably is no Users.initializeEvent event in your app, at least that's what your InitializeEventListener code suggests.

    2. Instantiating a table class will cause the Model.initialize event to be triggered, so even if you would have properly added your listener afterwards, it would never get triggered unless you would have cleared the table registry, so that the users table would be newly constructed on the next get() call.