'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());
Solution 1:[1]
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.
Solution 2:[2]
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()
callsview()
somewhere. view()
callsapp()
to get the factory that creates Views1app()
callsContainer::make
Container::make
callsContainer::resolve
1Container::resolve
decides the Factory needs to be built and callsContainer::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 interfaceIlluminate\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 namemake
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
Solution 3:[3]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | mrhn |
Solution 2 | |
Solution 3 | Peter DeMarco |