Laravel unit testing automatic dependency injection not working? - laravel

With Laravel Framework 5.8.36 I'm trying to run a test that calls a controller where the __construct method uses DI, like this:
class SomeController extends Controller {
public function __construct(XYZRepository $xyz_repository)
{
$this->xyz_repository = $xyz_repository;
}
public function doThisOtherThing(Request $request, $id)
{
try {
return response()->json($this->xyz_repository->doTheRepoThing($id), 200);
} catch (Exception $exception) {
return response($exception->getMessage(), 500);
}
}
}
This works fine if I run the code through the browser or call it like an api call in postman, but when I call the doThisOtherThing method from my test I get the following error:
ArgumentCountError: Too few arguments to function App\Http\Controllers\SomeController::__construct(), 0 passed in /var/www/tests/Unit/Controllers/SomeControllerTest.php on line 28 and exactly 1 expected
This is telling me that DI isn't working for some reason when I run tests. Any ideas? Here's my test:
public function testXYZShouldDoTheThing()
{
$some_controller = new SomeController();
$some_controller->doThisOtherThing(...args...);
...asserts...
}
I've tried things like using the bind and make methods on app in the setUp method but no success:
public function setUp(): void
{
parent::setUp();
$this->app->make('App\Repositories\XYZRepository');
}

That's correct. The whole idea of a unit test is that you mock the dependant services so you can control their in/output consistently.
You can create a mock version of your XYZRepository and inject it into your controller.
$xyzRepositoryMock = $this->createMock(XYZRepository::class);
$some_controller = new SomeController($xyzRepositoryMock);
$some_controller->doThisOtherThing(...args...);

This is not how Laravels service container works, when using the new keyword it never gets resolved through the container so Laravel cannot inject the required classes, you would have to pass them yourself in order to make it work like this.
What you can do is let the controller be resolved through the service container:
public function testXYZShouldDoTheThing()
{
$controller = $this->app->make(SomeController::class);
// Or use the global resolve helper
$controller = resolve(SomeController::class);
$some_controller->doThisOtherThing(...args...);
...asserts...
}
From the docs :
You may use the make method to resolve a class instance out of the
container. The make method accepts the name of the class or interface
you wish to resolve:
$api = $this->app->make('HelpSpot\API');
If you are in a location of your code that does not have access to the
$app variable, you may use the global resolve helper:
$api = resolve('HelpSpot\API');
PS:
I am not really a fan of testing controllers like you are trying to do here, I would rather create a feature test and test the route and verify everything works as expected.
Feature tests may test a larger portion of your code, including how
several objects interact with each other or even a full HTTP request
to a JSON endpoint.
something like this:
use Illuminate\Http\Response;
public function testXYZShouldDoTheThing()
{
$this->get('your/route')
->assertStatus(Response::HTTP_OK);
// assert response content is correct (assertJson etc.)
}

Related

Laravel not picking up Mocked class

In my controller I am using a class like so:
public function book($id, CalRequest $request)
{
$event = $this->repo->find($id);
(new CalAuthorisation())
->setEvent($event)
->canBook();
$this->booking->book($event, $request);
}
I'm mocking a class like below:
$auth = \Mockery::mock(CalAuthorisation::class)->makePartial();
$this->app->bind(CalAuthorisation::class, function() use ($auth) {
return $auth;
});
$auth->shouldReceive('canBook')->once()->andReturn(true);
However, if I dd within the canBook() function I still get into there. I have a similar mock in place for the CalRequest class which works fine. Any suggestions as to why this isn't working!
Dependency injection is about injecting different implementation into your logic. When using new keyword, PHP will fetch a certain class and only that class. Laravel solves dependency injection through the container, so you have to use the container for it to work.
There is two ways, either use app() or resolve(). Secondly you can inject your classes into methods that are on Laravel classes like jobs, commands, controllers etc. and this will be resolved through the container.
Using resolve().
resolve(new CalAuthorisation())
->setEvent($event)
->canBook();
Injecting it, through the controller method.
public function book($id, CalRequest $request, CalAuthorisation $calAuth)
{
$event = $this->repo->find($id);
$calAuth->setEvent($event)
->canBook();

Mocking a service class inside controller

I am trying to write a Feature test for my controller. To simplify my current situation, imagine my controller looks like this:
public function store(Business $business)
{
try {
(new CreateApplicationAction())->execute($business);
} catch (Exception $e) {
return response()->json(['message' => 'error'], 500);
}
return response()->json(['message' => 'success']);
}
What I am trying to achieve is, instead of testing CreateApplication class logic inside my integration test, I want to write another unit test for it specifically.
Is there a way I can simply say CreateApplicationAction expects execute() and bypass testing inside it? (without executing the code inside execute())
/** #test */
public function can_create_application()
{
$business = Business:factory()->create();
$mock = $this->mock(CreateApplicationAction::class, function (MockInterface $mock) use ($business) {
$mock->shouldReceive('execute')
->once()
->with($business)
->andReturn(true);
});
$response = $this->post('/businesses/3/application', $data);
$response->assertOk();
}
I saw online that people create "MockCreateApplicationAction" class but if possible I don't want to create another class as I don't want any logic to be inside it at all.
Is it possible?
class CreateApplicationAction
{
public function execute($business) {
dd("A");
// Business Logic...
}
}
So when I do the Mock, dd() should never be called. Or I am going in the wrong direction?
You will need to use Laravels container to resolve your class. The basic approach is to use the resolve() method helper. PHP does not have dependency injection, so you need to use one to make it possible, in Laravel the container solves that.
resolve(CreateApplicationAction::class)->execute($business);
On constructors, controller methods, jobs, events, listeners and commands (rule of thumb if the method is named handle), you can inject classes into the parameters and they will resolve through the container.
public function store(Business $business, CreateApplicationAction $applicationAction)
{
try {
$applicationAction->execute($business);

partially mocking a class without affecting the private properties in PHP

I have a class with a lot of methods in which I need to mock only one method due to some sql incompatibility between mysql and in-memory sqlite database.
class OrderService implements OrderServiceContract
{
protected $deliveryService;
public function __construct(Delivery $deliveryService) // DI injected object
{
...
$this->deliveryService = $deliveryService;
...
}
public function methodNeedstoBeMocked()
{
....some sql related code...
}
public function returnToWarehouse($orderId)
{
DB::transaction(function() use ($orderId) {
...
$this->deliveryService->someOtherMethod($orderId); // problematic external service call
...
});
}
}
Now in my test I partially mock this class according to this doc link, and I call the returnToWarehouse from test but then it says that
Error : Call to a member function returnToWarehouse() on null.
meaning that the property $deliveryService doesn't exist on mock.
My test Implementation is as follows.
/**
* #test
*/
public function an_order_can_be_returned_to_warehouse()
{
...
...
$this->partialMock(OrderService::class, function ($mock) {
$mock->shouldReceive('methodNeedstoBeMocked')->andReturn(collect([]));
});
$orderService = app(OrderService::class);
$orderService->markOrderReturnedToWarehouse($order->id); // here is the problem gets triggered.
...
//assertions
}
What might be going wrong here? and what are some ways to mitigate this? Appreciate your help in advance.
The issue here is that partial test doubles from Mockery do not call the original constructor. For more information, please read the documentation here.
Alternatively, you could consider mocking the "problematic" method a bit differently. For example, you could extract that logic to a repository (since you mention that it is dealing with the database layer) that can then be mocked during your test.
Usually, When I have to mock some third party services I set up a Mock Like this.
This way you can set up easily your DI services
<?php
if (app()->environment('testing')) {
$this->app->bind(Delivery::class, static function () {
$service = \Mockery::mock(Delivery::class);
$service->shouldReceive('someOtherMethod')->andReturn([]);
return $service;
});
}

How to swap out dependency in laravel container

I have registered a Paypal service provider:
App\Providers\PaypalHelperServiceProvider::class,
and, when I type hint it in my controller it properly resolves:
public function refund(Request $request, PaypalHelper $paypal) {...
Here is my provider class:
class PaypalHelperServiceProvider extends ServiceProvider
{
protected $defer = true;
public function register()
{
$this->app->bind('App\Helpers\PaypalHelper', function() {
$test = 'test';
return new PaypalHelper();
});
}
public function provides()
{
$test = 'test';
return [App\Helpers\PaypalHelper::class];
}
}
Everything works as expected. Now I wanted to be able to modify controller to take a PayPal interface. I would then update my service provider to conditionally pass in either the real class or a mock one for testing, using the APP_ENV variable to determine which one to use. I put some debuggers into the service provider class and could not get it to ever go in. I thought perhaps that it only loads them on need, so I put a breakpoint inside my controller. The class did resolve, but it still never went into the service provider class! Can someone explain to me why this is the case? Even when I modified the code to pass in a different class type it did not pick up.
EDIT:
Here is the code flow I see when I debug this:
ControllerDispatcher -> resolveClassMethodDependencies -> resolveMethodDependencies -> transformDependency. At this point we have the following laravel code in the RouteDependencyResolveerTrait:
protected function transformDependency(ReflectionParameter $parameter, $parameters, $originalParameters)
{
$class = $parameter->getClass();
// If the parameter has a type-hinted class, we will check to see if it is already in
// the list of parameters. If it is we will just skip it as it is probably a model
// binding and we do not want to mess with those; otherwise, we resolve it here.
if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
return $this->container->make($class->name);
}
}
Since getClass() always resolves to the interface name, when we call container->make(), it always fails with
Target [App\Helpers\PaypalHelperInterface] is not instantiable.
Change
$this->app->bind('App\Helpers\PaypalHelper', function() {
$test = 'test';
return new PaypalHelper();
});
To
if (app()->environment('testing')) {
$this->app->bind(
PaypalHelperInterface::class,
FakePaypalHelper::class
)
} else {
$this->app->bind(
PaypalHelperInterface::class,
PaypalHelper::class
);
}
I finally found out the issue. My problem was that my provider wasn't being picked up at all. Here was the solution
composer dumpautoload
php artisan cache:clear
https://laravel.io/forum/02-08-2015-laravel-5-service-provider-not-working

Why won't this Laravel 5.4 service provider register?

I am trying to do a hello world service provider with the new Laravel 5.4.
I have created the following service provider file:
//File: app/TestProvider/TestServiceProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
$this->app->bind('Test', function ($app) {
return new Test();
});
}
}
I have created a simple class under the same namespace:
//File: app/TestProvider/Test.php
namespace App\TestProvider;
class Test
{
/**
* Register bindings in the container.
*
* #return void
*/
public function helloWorld()
{
echo "hello world";
}
}
The problem is, this is not registering. The register method is executing as when I put a breaker before the 'bind' method, it executes:
public function register()
{
dd("BREAKER");
$this->app->bind('Test', function ($app) {
return new Test();
});
}
So this outputs "BREAKER" as expected. However if I put the breaker in the closure, nothing happens which suggests for some reason, that 'bind' method isn't being executed??
Any ideas?
EDIT:
Just some further info: I know that the Test class is registered and in the correct namespace as I can do:
dd(new Test());
in the registration method, and it outputs the resource id as expected.
Explanation
The closure provided only runs when the binding is being resolved. That's why it's a closure, it can be saved in the service container and resolved at any time while the program runs.
Solution
To see the resolved binding, create a controller and resolve the class in that controller:
// File: app/Http/Controllers/TestController.php
namespace App\Http\Controllers;
// This isn't the best way, but it works. See the best way below
class TestController extends Controller {
public function index()
{
return \App::make('Test')->helloWorld();
}
}
Of course, don't forget to register the route:
// File: routes/web.php
Route::get('/', 'TestController#index');
The binding will resolve when you hit the homepage.
However, as I said, it's not the best way, so here I prepared a better way. Change the way you register the binding:
// File: app/Providers/TestProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
use App\TestProvider\Test;
// Better way
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
// Note: we bind the exact complete class name!
$this->app->bind(Test::class, function ($app) {
return new Test();
});
}
}
After this change the controller so that it looks like this:
namespace App\Http\Controllers;
use App\TestProvider\Test;
class TestController extends Controller {
/**
* #var Test $test
*/
private $test;
// Let Laravel resolve the dependency on constructing the class
public function __construct(Test $test)
{
$this->test = $test;
}
public function index()
{
return $this->test->helloWorld();
}
}
You will see that the exact same thing happens, but it looks more elegant and avoids conflicts.
Details
Laravel gives only a high level overview of the service container, which doesn't help to learn how it works on the inside. The best way to see that is to go down the call stack.
When you do that, you find that Laravel registers every class in the project in the service container. That means that whether you create a service provider or not, the class will be in the container. How exactly?
When you run php artisan optimize, Laravel creates files that have array with all the classes of the project. When you run the app, after registering everything from the service providers, Laravel registers the rest of the classes from that file.
That means that in your case, if you don't specifically register the Test class, it will still be resolvable. Basically, you only need to register classes that need some specific instructions to be resolved.
So how does Laravel resolve the dependencies?
When you run \App::make(Test::class) or inject dependency via type hinting in the constructor (the "better way" from my solution), Laravel looks for that dependency among the bindings.
When it finds the dependency, it resolves either the closure associated to it or the constructor of the class directly.
When it resolves the constructor directly, it looks for type hints among the constructor parameters and recursively resolves all of them until there's nothing else to resolve.
After that it returns the resolved class.
Of course, bear in mind that for Laravel to analyze the contructor of a class, it needs to be resolved via the service container in the first place. You can't just call $test = new Test(); and expect Laravel to do all the magic :)
Conclusion
This is a rather quick overview of Laravel's service container. The best way for you to learn it is, of course, studying the sources for yourself. It's truly elegant and it uses PHP's functionality to the fullest.
I really hope this shed some light on the service container for you and can help you in the future :)
The closure passed to the bind() method is not executed until you actually attempt to resolve the alias you are binding.
So, if you dd('breaker') inside the closure, this won't actually get executed until Test is resolved (whatever your preferred resolution method is):
Service provider:
// bind the closure to the 'Test' alias
public function register()
{
$this->app->bind('Test', function ($app) {
dd("BREAKER");
return new Test();
});
}
Code that resolve Test alias:
// different ways of resolving the alias out of the container.
// any of these will execute the bound closure.
$test = resolve('Test');
$test = app('Test');
$test = app()->make('Test');
$test = \App::make('Test');
try:
$this->app->bind(Test::class, function ($app) {
return new Test();
});

Resources