Laravel unit testing automatic dependency injection not working?

Viewed 2147

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');
}
2 Answers

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