Lumen IoC binding resolution spotty within phpunit tests - php

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?

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.

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
);

Related

PHPUnit gives error: Target [Illuminate\Contracts\View\Factory] is not instantiable

I created a simple test for my new Laravel 7 application. But when I run php artisan test I get the following error.
Target [Illuminate\Contracts\View\Factory] is not instantiable.
The error doesn't appear when I go to the page in the browser.
$controller = new HomeController();
$request = Request::create('/', 'GET');
$response = $controller->home($request);
$this->assertEquals(200, $response->getStatusCode());
Although "Just write feature tests" may seem like a cop-out ("They're not unit tests!"), it is sound advice if you do not want to get bogged down by framework-specific knowledge.
You see, this is one of those problems that come from using facades, globals, or static methods. All sorts of things happen outside of your code (and thus your test code) in order for things to work.
The problem
To understand what is going on, you first need to know how Laravel utilizes Containers and Factories in order to glue things together.
Next, what happens is:
Your code (in HomeController::home() calls view() somewhere.
view() calls app() to get the factory that creates Views1
app() calls Container::make
Container::make calls Container::resolve1
Container::resolve decides the Factory needs to be built and calls Container::build to do so
Finally Container::build (using PHP's ReflectionClass figures out that \Illuminate\Contracts\View\Factory can not be Instantiated (as it is an interface) and triggers the error you see.
Or, if you're more of a visual thinker:
The reason that the error is triggered is that the framework expects the container to be configured so that a concrete class is known for abstracts (such as interfaces).
The solution
So now we know what is going on, and we want to create a unit-test, what can we do?
One solution might seem to not use view. Just inject the View class yourself! But if you try to do this, you'll quickly find yourself going down a path that will lead to basically recreating loads of framework code in userland. So not such a good idea.
A better solution would be to mock view() (Now it is really a unit!). But that will still require recreating framework code, only, within the test code. Still not that good.[3]
The easiest thing is to simply configure the Container and tell it which class to use. At this point, you could even mock the View class!
Now, purists might complain that this is not "unit" enough, as your tests will still be calling "real" code outside of the code-under-test, but I disagree...
You are using a framework, so use the framework! If your code uses glue provided by the framework, it makes sense for the test to mirror this behavior. As long as you don't call non-glue code, you'll be fine![4]
So, finally, to give you an idea of how this can be done, an example!
The example
Lets say you have a controller that looks a bit like this:
namespace App\Http\Controllers;
class HomeController extends \Illuminate\Routing\Controller
{
public function home()
{
/* ... */
return view('my.view');
}
}
Then your test[5] might look thus:
namespace Tests\Unit\app\Http\Controllers;
use App\Http\Controllers\HomeController;
use Illuminate\Contracts\View\Factory;
class HomeControllerTest extends \PHPUnit\Framework\TestCase
{
public function testHome()
{
/*/ Arange /*/
$mockFactory = $this->createMock(Factory::class);
app()->instance(Factory::class, $mockFactory);
/*/ Assert /*/
$mockFactory->expects(self::once())
->method('make')
->with('my.view')
;
/*/ Act /*/
(new HomeController())->home();
}
}
A more complex example would be to also create a mock View and have that be returned by the mock factory, but I'll leave that as an exercise to the reader.
Footnotes
app() is asked for the interface Illuminate\Contracts\View\Factory, it is not passed a concrete class name
The reason Container::make does nothing other than call another function is that the method name make is defined by PSR-11 and the Laravel container is PSR compliant.
Also, the Feature test logic provided by Laravel already does all of this for you...
Just don't forget to annotate the test with #uses for the glue that is needed, to avoid warnings when PHPUnit is set to strict mode regarding "risky" tests.
Using a variation of the "Arrange, Act, Assert" pattern
This is not how you test endpoints in Laravel. You should let Laravel instantiate the application as it is already setup in the project, the examples you can see here.
What you already wrote can be rewritten to something like this.
$response = $this->call('GET', route('home')); // insert the correct route
$response->assertOk(); // should be 200
For the test to work, you should extend the TestCase.php, that is located in your test folder.
If you're finding this in The Future and you see #Pothcera's wall of text, here's what you need to know:
The ONLY reason he's doing any of that and the ONLY reason you're seeing this in the first place in a Unit test is because he and you haven't changed from PHPUnit\Framework\TestCase to Tests\TestCase in the test file. This exception doesn't exist when you extend the test case that includes app().
My advice would be to simply extend the correct base test case and move on with your life.

How to use Container to access parameters on a custom Exception

I have been struggling a bit to understand how can I make use of the Container that gives me access to the parameters located at config/parameters.yml.
The problem in my hands is I created some custom Exceptions and I created a library that slacks me every time one of those exceptions is triggered. For that, I need to send the container to the Logger in order to be able to pull those through within the library.
But I am failing to be able to grab a hold on the container. I have an idea it needs to be injected and that I could achieve such in the config/services.yml but I am failing to understand how.
So far this is what I have come to achieve:
My Custom Exception Parent
Because all exceptions extend from the parent, they will all trigger the Logger. And it is at this point I need the container to exist:
abstract class CriticalLogAlertException extends \RuntimeException
{
protected $logger;
public function __construct ($message = "", $code = 0, Throwable $previous = NULL)
{
parent::__construct($message, $code, $previous);
Log::critical(
$this->message,
[],
AbstractCredentialsFactory::YAML_TYPE,
'PARAMETERS CONTAINER ACCESS NEEDS TO BE ADDED HERE'
);
}
abstract public function generateMessage($message) : string;
}
What I though was on creating on this class a method setContainer() that I could use in my config/services.yml:
public function setContainer(ContainerInterface $container)
{
$this->container - $container;
}
So at this point I could create a property in the abstract class and use that to pass it to the library as it would be already available at class execution. Though I am not too certain this is achievable or correct;
My config/services.yml
Below is the code I added to my services container:
ExceptionContainer:
class: AppBundle\Exception\CriticalLogAlertException
calls:
- [ setContainer,[ #service_container ] ]
Could someone help me understanding if it something I am missing or misunderstanding in order to set it available?
NOTE: If there is anything else required to better understand my problem, please let me know so I can update my question :)
Thank you in advance!
Sooo, this took me a while but I had to dive deeper in order to understand, which meant to go through some fights and research before being able to start understanding it a bit better.
Was left for a while even though I got it working a while back, but for those wondering the same and new to the process let me share a bit :)
Because I was instantiating the exceptions with throw new meant I had direct access to the __construct method. This meant, if my exception was expecting a ContainerInterface I had to provide one.
Now, two case scenarious could work with this.
1. Injection
With Injection I can autowire the requirements either by using an available service (which we can check with below command);
php bin/console debug:autowiring
Or, if injecting a custom class, I needed to specify where, which and what it provides at services.yml
An possible example of what I am refering to add to services.yml can be seen below:
TestException:
class: AppBundle\Exception\TerminationAmlkyc
arguments: ['#service_container']
public: true
2. Instantiation
In my case, I either transferred the ContainerInterface whenever I needed it for a new class or was able to Instanciated as I referenced above.
Because my origin was through a ContainerAwareCommand I could make use of $this->getContainer() to retrieve it and then pass it to the exceptions whenever needed.
throw new TestException($this->getContainer())

Correct unit testing

I started using unit and functionality test with this project and because of this I have some questions:
Im working with the symfony php framework. And I have a doctrine like LDAP ORM service.
Furthermore I have a user repository (as a service) which depends on the LDAP ORM service, the logger and a validation service.
Now I want to write a unit test for the addUser function of the UserRepo.
It will internally call: getNewUidNumber, userToEntities, doesUserExist and getUserByUid.
My question is:
Should I mock all these internal function to just test the addUser function? Would that be against the unit test idea (just test the API).
Or should I just mock the LDAP ORM service, the Logger, and the validation service, so that the class calls all internal functions? But this would cause a huge test function with a lot of mocking because I have to mock the repositories for all internal repositories calls.
Or should I start the symfony kernel and use the ServiceContainer to use the ORM LDAP service with a real test database. But wouldn't that be a functionally test and not a unit test?
I heard that its bad to have so many dependencies in a test. So I thought it would be bad to use the whole serviceContainer.
Adduser:
public function addUser(User $user)
{
$pbnlAccount = $this->userToEntities($user);
if(!$this->doesUserExist($user)) {
$pbnlAccount->setUidNumber($this->getNewUidNumber());
$this->ldapEntityManager->persist($pbnlAccount);
$this->ldapEntityManager->flush();
}
else {
throw new UserAlreadyExistException("The user ".$user->getUid()." already exists.");
}
return $this->getUserByUid($user->getUid());
}
For more code, like the internal functions:
https://gist.github.com/NKPmedia/4a6ee55b6bb96e8af409debd98950678
Thanks
Paul
First, I would like to rewrite the method a tiny bit, if I may.
public function addUser(User $user)
{
if ($this->doesUserExist($user)) {
throw new UserAlreadyExistException("The user ".$user->getUid()." already exists.");
}
// ... shortened for brevity
$pbnlAccount = $this->userToEntities($user);
$this->ldapEntityManager->persist($pbnlAccount);
}
The other relevant method is:
private function doesUserExist(User $user)
{
$users = $this->ldapRepository->findByUid($user->getUid());
return count($users) === 1;
}
Immediately we can see that we basically have two tests:
We test that the method throws when the user exists
We test that the method persists a PbnlAccount if the user does not exist.
If you do not see why we have these two tests, note that there are 2 possible "flows" in this method: one where the block inside the if statement is executed, and one where it is not executed.
Lets tackle the first one:
public function testAddUserThrowsWhenUserExistsAlready()
{
$user = new User();
$user->setUid('123');
$ldapRepositoryMock = $this->createMock(LdapRepository::class);
$ldapRepositoryMock
->method('findByUid')
->expects($this->once())
->with('123')
->willReturn(new PbnlAccount());
$userRepository = new UserRepository($ldapRepositoryMock);
$this->expectException(UserAlreadyExistException::class);
$userRepository->addUser($user);
}
The second test is left as an exercise for the reader :)
Yes you will have to do some mocking in your case. You wil need to mock the LdapRepository and LdapEntityManager both in this case.
Note 1: this code probably is not runnable, since I do not know the exact details of your code base (and I wrote this off the top of my head), but that is beside the point. The point is that you want to test for the exception.
Note 2:
I would rename your function to createNewPbnlAccountForUser(User $user) which is longer, but more descriptive of what it actually does.
Note 3:
I am not sure why you are returning $this->getUserByUid() since that seems redundant (you already have the User right there), so I am ommitting that case.
You need to mock ldapEntityManager and all repository services but not the internal function.And as you said, don't boot kernel in unit test. So, you should test all cases with success and throwing exception (make sure to check all behaviour)
If you want to perform a unit test, you should mock all collaborators.
Now, entity managers, ldap services and so on should not be mocked (read more here).
Moreover, if you happen to be in a situation where the Arrange part (set mocks, stubs, and so on) is painful and take "a lot" of the test, maybe this is a smell that your class has too many responsibility (is doing too much things).
That said, when I do unit test, I would like the test to fail only for an internal (to the class) reason, not because I've changed a collaborator line that messes up all my tests.

Mockery "shouldReceive" yet method doesn't exist

I'm trying to understand Tests and Mockery a bit more with Laravel. I have a repository pattern setup, which my controller users. I want to test my basic getAllUsers()method:
public function test_get_all_users_method()
{
$repo = Mockery::mock('Acme\Repositories\User\UserRepository');
$repo->shouldReceive('all')->once()->andReturn('foo');
$controller = new Acme\Controllers\Api\UserController($repo);
$response = $controller->getComponents();
$this->assertEquals('foo', $response);
}
As I understand it, I'm mocking my UserRepository, and I expect my UserRepository to have it's all() method hit. This returns some dummy data and I expect to see this in my response output.
So that works fine. The all() method exists in my Eloquent implementation of the repository. However, if I remove the all() method, the test still passes... Why would it? Surely the test should fail.
If this is normal, I'm struggling to understand why I'd test my controller like this, since I could pass any old method name into it even if it exists or not.
Cheers
That's how mockery operates by default, I like it that way because it allows me to develop by wishful thinking, i.e. I wish my UserRepository interface had an all method.
You can tell mockery to disallow it though, it's a bit ugly, but you can put this in your test bootstrap file:
\Mockery::getConfiguration()->allowMockingNonExistentMethods(false);
You could also set this up to control it with an environment variable or something, so you allow mocking non-existent methods during normal use, but prevent it on your continuous integration run etc.

Laravel4: Use different model depending on config

I extended my Laravel4 Project with another Project Model. Now I have two Models (like Model1 and Model2). My goal is to set a custom config var, telling the installation which Project model to use.
Is there a way to set this in the config and then use Project1 as Project in my Controllers?
If I call $p = new Project; I want it to be an instance of either Project1 or Project2, depending on the configuration.
I hope you understand my question, thank you
You can kind of achieve this by using the IoC container.
First, instead of using new Project, use App::make('Project') - that won't change anything yet, but will allow you to now 'redefine' what 'Project' points to.
So the next step is, in your startup script (or a service provider if you're using service providers) code similar to the following:
if (Config::get('model', 'Project') != 'Project') {
App::alias('Project', Config::get('model', 'Project'));
}
This line will bind whatever classname you set in 'model' in the main config to be what the code retrieves when you use App::make('Project'). The reason for the if is just so we don't end up aliasing Project to Project. Feel free to test it without, but it may create an infinite loop/recursion in the IoC.
The code may not be perfect as it's untested, but at the very least it should give you a path to explore.
Points to note
You may find App::bind('project', Config::get('model')); works better, especially if you start using the IoC container's automatic injection
As mentioned above, the if surrounding the magic line may not be required. The IoC container may well be clever enough to not keep following an alias to itself.
A note on static access
I mentioned before to change all instances of new Project to App::make('Project'), but I didn't mention how to change your code when using something like $projects = Project::all();. Well you can still do a similar thing, but you just have to get an instance: App::make('Project')->all();.
When you start going down this route, you find that you're having to App::make('Project') more and more. When this is the case, using Laravel's automatic dependency injection will help a lot. To do this, you typehint the class in your controller's constructor:
class ProjectController extends \Controller
{
protected $project;
public function __construct(Project $project)
{
$this->project = $project;
}
public function index()
{
$projects = $this->projects->whereVisible(1)->orderBy('date', 'desc')->get();
return View::make('projects.index', compact('projects'));
}
}
This automatic injection happens in a few classes. I don't know the full list, but certainly controllers, event listeners, view creators and view composers. If you have a class of your own you wish to take advantage of the automatic injection, you can App::make() it and it too will get its dependencies injected automatically.

Categories