3

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 2

2

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...);
Sign up to request clarification or add additional context in comments.

5 Comments

So the automatic injection doesn't work with tests? laravel.com/docs/5.8/container#automatic-injection I don't want to create a mock for this repository and actually pass it to the SomeController __construct method.
How do you handle this error: Class "SomeOtherClass" is declared "final" and cannot be mocked
You could of course create a real instance of the repository, but you would have to do all of it without the automatic injection (meaning you probably will end up creating tons of objects which is really not what you're looking to do. If you really want to test the controller in full, maybe the thing you're looking for in an integration test. You could call your own endpoint and let the controller run its natural course that way.
Testing final classes is really annoying. You could have a look at that class and decide if it really has to be final. If it should then you can find some ways to do so here: tomasvotruba.com/blog/2019/03/28/…
Thanks Dirk, the final class is a third party api plugin. I figured I would just not use DI for that class.
2

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.)
}

2 Comments

I 100% agree with the testing of controllers. I should be using feature tests to test the controller. I will update the tests to do so.
Features tests are not a general replacement for unit tests. This maybe fine for controllers, but should not replace unit testing as a whole.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.