Lumen IoC binding resolution spotty within phpunit tests

941 views Asked by At

I've run into an issue with Lumen v5.0.10 that has me at my wits end. I'm designing an application largely using TDD with the bundled phpunit extensions. I'm basically getting a BindingResolutionException for "App\Contracts\SubscriberInteractionInterface". This is an interface in the directory App\Contracts which has an implementation in App\Services which is registered in the AppServiceProvider like so:

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {

        // Owner manager
        $this->app->singleton(
            'App\Contracts\OwnerInteractionInterface',
            'App\Services\OwnerManager'
        );

        // Subscriber manager
        $this->app->singleton(
            'App\Contracts\SubscriberInteractionInterface', 
            'App\Services\SubscriberManager'
        );

//      dd($this->app->bound('App\Contracts\SubscriberInteractionInterface'));
    }
}

My frustration is that if I uncomment that last line in the function then it shows that App\Contracts\SubscriberInteractionInterface has been bound (and thus may be resolved).

I then have a controller which effectively looks like this

class MyController extends Controller {

    public function __construct(LoggerInterface $log)
    {
        $this->log = $log;
    } 


    public function index(Request $request)
    {
           if (/* Seems to come from owner */)
           {
               $owners = App::make('App\Contracts\OwnerInteractionInterface');
               return $owners->process($request);
           }

           if (/* Seems to come from subscriber */)
           {
               $subscribers = App::make('App\Contracts\SubscriberInteractionInterface');
               return $subscribers->process($request);
           }
    }
}

I use them in this way because I only want the relevant one instantiated (not both as would happen if I type-hinted them) and they also each have type hinted dependencies in their constructors.

The issue is that the route of the tests that needs OwnerInteractionInterface runs just fine but the one that needs SubscriberInteractionInterface does not. The implementations and interfaces are largely similar and as I showed before, they are both registered at the same time and I can confirm that SubscriberInteractionInterface is bound. In fact, if I put the line:

dd(App::bound('App\Contracts\SubscriberInteractionInterface'));

at the top of index() it returns true. The tests happen to be ordered such that the path that uses OwnerInteractionInterface runs first and it resolves fine and then the other test fails with a BindingResolutionException. However, if I omit other tests and run just that one, then everything goes smoothly. The tests are in different files and the only setup I do is to bind a mock for a third party API in place of an entirely different binding from those shown and none of that code touches these classes. This is done within a setUp() function and I make sure to call parent::setUp() within it.

What's going on here? Could it be that binding one concrete instance wipes non-concrete bindings from the IoC? Or is it that the default setup allows some influence to transfer over from one test to another?

I know I sorta have a workaround but the constraint of never running the full test-suite is annoying. Its starting to seem that testing would be easier if I just use the instance directly instead of resolving it from its interface.

Also, does anyone know a way to inspect the IoC for resolvable bindings?

2

There are 2 answers

0
jeteon On BEST ANSWER

After further attempts at debugging, I've found that if you use app(...) in place of App::make(...) then the issue does not come up. I put in a eval(\Psy\sh()) call in the tearDown of the TestCase class and found that after a few tests you get the following result:

>>> app()->bound('App\Contracts\OwnerInteractionInterface')
=> true
>>> App::bound('App\Contracts\OwnerInteractionInterface')
=> false
>>> App::getFacadeRoot() == app()           
=> false 

This is to say that somehow, the Laravel\Lumen\Application instance that the App facade uses to resolve your objects is not the same as the current instance that is created by the setUp() method. I think that this instance is the old one from which all bindings have been cleared by a $this->app->flush() call in the tearDown() method so that it can't resolve any custom bindings in any tests that follow the first tearDown() call.

I've tried to hunt down the issue but for now I have to conclude this project with this workaround. I'll update this answer should I find the actual cause.

1
krisanalfa On

Instead of use bind, you can use bindIf method. Container will check whether the abstract has been bound or not. If not, it will bind your abstract and vice versa. You can read the api here.

So if you use singleton, you may use bindIf like.

// Owner manager
$this->app->bindIf(
    'App\Contracts\OwnerInteractionInterface',
    'App\Services\OwnerManager',
    true
);

// Subscriber manager
$this->app->bindIf(
    'App\Contracts\SubscriberInteractionInterface', 
    'App\Services\SubscriberManager',
    true
);