mardi 5 septembre 2017

Testing custom events with Lumen 5.4

First and foremost I apologize for the length of my question it is to provide context.

I have created a project that uses Laravel's micro-framework known as Lumen. How I am taking advantage of the framework is totally different from the normal routine. Below is my composer file:

    {
      "name": "refinecraft/identityserver",
      "config":{
         "vendor-dir": "ext"
      },
      "autoload":{
         "psr-4":{ "RefineCraft\\IdentitySystem\\": "src/" },
         "exclude-from-classpath": [
            "src/Tests/"
          ]
      },
      "require-dev": {
         "phpunit/phpunit": "^6.3"
      },
     "require": {
       "laravel/lumen": "^5.4"
    }
}

As you can see with composer.json file Lumen has been added as a required package instead of following the normal routine. Guided by the app.php file found in LUMEN_DIR/boostrap/ I created an event class called ApplicationStarted with two listeners. These listeners are named ContainerBindingsRegister and WebAppRoutesRegister respectively.

Below are all the classes I have mentioned above:

namespace RefineCraft\IdentitySystem\Application\Events;

use Illuminate\Queue\SerializesModels;

abstract class ApplicationEvent  implements ApplicationEvents
{
    use SerializesModels;
}

Since I intend to modularize the application I am intending of using the constants defined within the ApplicationEvents interface. Below is the interface:

namespace RefineCraft\IdentitySystem\Application\Events;

interface ApplicationEvents
{
    const APP_STARTED = 'app.start';

    const APP_STOPPED = 'app.stop';
}

At this point I have written the first event ApplicationStarted event as follows:

use RefineCraft\IdentitySystem\Application\Events\ApplicationEvent;
use Laravel\Lumen\Application;

class ApplicationStarted extends ApplicationEvent
{
    /**
     * @var Application
     */
    protected $app;

    /**
     * ApplicationStarted constructor.
     *
     * @param Application $app
     * @return void
     */
    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    /**
     * Gets the Application instance so that it can be passed
     * onto listeners
     *
     * @return Application
     */
    public function getApplication()
    {
        return $this->app;
    }
}

I have added comments to explain my intentions with this event class. Following are my listeners as mentioned.

namespace RefineCraft\IdentitySystem\Application\Events\Application\Listeners;

use RefineCraft\IdentitySystem\Application\Events\Application\ApplicationStarted;

class ContainerBindingsRegister
{
    protected $defaultBindings = array(
        \Illuminate\Contracts\Debug\ExceptionHandler::class => \App\Exceptions\Handler::class,
        \Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class,

    );

    public function handle(ApplicationStarted $event)
    {
        foreach($this->defaultBindings as $abstraction => $concrete){
            $event->getApplication()->singleton($abstraction, $concrete);
        }
    }
}

namespace RefineCraft\IdentitySystem\Application\Events\Application\Listeners;

use RefineCraft\IdentitySystem\Application\Events\Application\ApplicationStarted;

class WebAppRoutesRegister
{
    protected $defaultRouteGroup = array(
        'namespace' => 'RefineCraft\IdentitySystem\Application\Http\Controllers',
    );

    public function handle(ApplicationStarted $event)
    {
        $drg = $this->defaultRouteGroup;
        $app = $event->getApplication();

        $app->group($drg, function ($app) {
            require __DIR__.'/../../../Http/Resources/routes/web.php';
        });
    }
}

To make things easier for myself I created a trait that I can use in testing as well for integration purposes. I had to extend Lumen's TestCase class which defines an abstract method called createApplication. Below is how I implement that abstract method with the help of the trait:

namespace RefineCraft\IdentitySystem\Application\Actions;

use Laravel\Lumen\Application;

trait CreatesApplication
{
    /**
     * This method accepts an Application Path that should help
     * when testing the Application.
     *
     * @param null $appPath
     * @return Application
     */
    public function getApplication($appPath = null)
    {
        if(is_null($appPath) || !is_dir($appPath)){
            $appPath = realpath(__DIR__.'/../');
        }

        return new Application($appPath);
    }
}

use RefineCraft\IdentitySystem\Application\Actions\CreatesApplication;
use Laravel\Lumen\Testing\TestCase;

class IntegrationTestCase extends TestCase
{
    use CreatesApplication;

    public function createApplication()
    {
        return $this->getApplication();
    }
}

NB: My test case classes do not use any namespaces as I feel it is not necessary. I stand to be correct on this view as I believe it only is useful if you want to implement some namespaced fixtures.

The first test case I wrote to assert that the application created through my trait is an instance of Lumen's passes:

require_once __DIR__ . '/../IntegrationTestCase.php';

use Laravel\Lumen\Application;

/**
 * Class ApplicationIntegrationTest
 *
 * @description Tests accessibility to our Application Instance
 */
class ApplicationIntegrationTest extends IntegrationTestCase
{
    /** @test */
    public function it_can_create_application_instance()
    {
        $this->assertInstanceOf(Application::class, $this->app);
    }
}

Again the test class above does not test anything else other than that we can be able to create the application instance. The idea was to move along to testing my event class whether it is fired successfully without calling any of the listeners and the write test cases for each of the listeners to assert that whatever they do actually works, i.e., for example to bind the Kernel classes and mounting routes respectively. The last stop of this exercise would be to update the ApplicationIntegrationTest with an assertion that we can reach a certain route within our controller as defined within the WebAppRoutesListener class.

Having followed this tutorial I ended up with the test case shown below:

require_once __DIR__ . '/../../../../IntegrationTestCase.php';

use RefineCraft\IdentitySystem\Application\Events\Application\ApplicationStarted;

class ApplicationStartedTest extends IntegrationTestCase
{
    public static $events;

    public function setUp()
    {
        parent::setUp();

        static::$events = array();
    }

    public function assertEventApplicationStartedFired($event = null)
    {
        $this->assertTrue(count(static::$events) >= 1, 'No event was fired');

        if (!is_null($event)) {
            $eventFired = array_first(
                static::$events,
                function ($index, $eventClass) use ($event) {
                    return get_class($eventClass) == $event;
                }
            );

            $this->assertNotNull($eventFired, "No event {$event} was fired");
        }
    }

    /** @test */
    public function it_can_start_application()
    {
        $this->assertEventApplicationStartedFired(ApplicationStarted::class);
    }
}

/**
 * Hijack Laravel's Global `event` function for testing purposes
 *
 * @param $event
 */
function event($event)
{
    ApplicationStartedTest::$events[] = $event;
}

The first issue I am getting here is that I cannot redeclare the event global function for my test case. I am not sure if it was not for this failure would my test case be running.

My question is how can I test for this workflow, i.e my event and its listeners? I kind of need this architecture for the planned future work that will be added in this code base.



via Chebli Mohamed

Aucun commentaire:

Enregistrer un commentaire