I have a Slim application that has some Middleware.
It performs authentication for the route, and retrieves the route like so:
$route = $this->getApplication()->router()->getCurrentRoute();
I'm now testing it and am attempting to use Mockery to mock the result of the chained call, so I can effectively specify the route.
$mock = M::mock('\Api\SessionMiddleware[getApplication]');
$mock->shouldReceive('router->getCurrentRoute')->andReturn('myRoute');
This doesn't work. It tries to call: require('lib/demeter/router.php') and fails as this doesn't exist.
I've also tried:
$mock = M::mock('\Api\SessionMiddleware')->shouldDeferMissing();
$mock->shouldReceive('getApplication->router->getCurrentRoute')->andReturn('myRoute');
This doesn't work either, failing with:
Failed opening required 'lib/demeter/getApplication.php'
What am I missing?
Even though there's an accepted answer, I wanted to provide future users with another solution.
There should be no need to mock the router if it's only used as an intermediate step of the demeter chain. Try this:
$mock = M::mock('\Api\SessionMiddleware');
$mock->shouldReceive('getApplication->router->getCurrentRoute')->andReturn('myRoute');
The key is removing the call to shouldDeferMissing(), which in this case seems to interfere with the demeter chain.
This way, a "pure" mock, which doesn't forward anything to the real implementation of SessionMiddleware, is returned. It should be able to reply with 'myRoute' when $mock->getApplication()->getRouter()->getCurrentRoute() is invoked.
You also need to mock the router. and let the router() method return the router mock in turn.
$mock = M::mock('\Api\SessionMiddleware[getApplication]');
$routerMock = M::mock('My\Router');
$routerMock->shouldReceive('getCurrentRoute')->andReturn('myRoute');
$mock->shouldReceive('router')->andReturn($routerMock);
Related
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.
These are my mocks:
$this->user->method('getCustomerId')
->willReturn($this->customerId);
$this->userToken->method('getUser')
->willReturn($this->user);
$this->securityContext->method('getToken')
->willReturn($this->userToken);
$this->securityContext->expects($this->once())
->method('isGranted')
->will($this->returnValue(true));
And this is the code of the class I am testing:
$token = $securityContext->getToken();
$isFullyAuthenticated = $securityContext->isGranted('IS_AUTHENTICATED_FULLY');
It throws the error:
Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException: The security context contains no authentication token. One possible reason may be that there is no firewall configured for this URL.
I have no idea what to do at this point, I though mocks intercepted the call to methods and returned whatever I wanted. But in this case is seems the isGranted methods is not being mock
I figured out that the problem is that the isGranted method is final, so it's imposible to mock, the workaround the problem was to mock all the attributes of SecurityContext so that when the method is call it returns my desired output, instead of intercepting the call with a mock method.
Try using willReturn instead of will and add the with
So try this:
$this->securityContext->expects($this->once())
->method('isGranted')
->with('IS_AUTHENTICATED_FULLY')
->willReturn(true);
Instead of this:
$this->securityContext->expects($this->once())
->method('isGranted')
->will($this->returnValue(true));
Hope this help
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.
Background: I'm working on an MVC framework for some practice, and want to make sure everything is 100% unit tested.
The setup currently is to have an instance of the application class (Ex_App). The main script asks a Dispatcher/Router for a controller name. This controller name is the name of a class implementing Ex_Controller. The result is returned as an instance of Ex_Dispatch_Result. This result is passed to the Ex_App instance using an invokeController($dispatchResult) function.
And this is where magic happens. The listing below is an excerpt:
$controllerName = $dispatchResult->getControllerName();
... checks for validaty of class name ...
$controller = new $controllerName();
$controller->prepare($this);
I'm using PHPUnit to do my unit testing, and am able to mock the dispatch result, correctly check that validating the class name of the controller works. The problem is how to check if prepare is called.
I'd like to do something similar to:
$mockController = $this->getMockBuilder('Ex_Controller')
->setMockClassName('Invoke_Correct_Controller')
->getMock();
$mockController->expects($this->once())->method('prepare');
However since a new instance of Invoke_Correct_Controller is created upon calling invokeController, it will not be this mock and thus the expects() call is completely irrelevant.
I could make the Ex_Dispatch_Result class responsible for returning a controller and testing that, but before returning an instance I will need to verify the correctness of the class name and in my opinion that responsibility should be with the Ex_App class and not the "dumb shell" Ex_Dispatch_Result class.
Is there something I am missing in the PHPUnit framework that I could use to test the code here, or some useful pattern that could work in my instance? I feel passing around controller names scales way better than passing around instances of controllers from the start, requiring the initialization of every possible controller. So, I kinda want to stick to passing around names and using the Ex_App as a factory for the controller instance.
Maybe I'm just over-thinking part of this problem, but that happens sometimes. It's why a fresh look by a third party often works :-)
There are couple of things you could do:
Extract controller creation logic to separate class e.g. ControllerFactory, and then mock controller factory instance, so that it returns your $mockController.
Extract controller creation logic to separate method and use partial mocking.
Return $mockController from $dispatchResult->getControllerName(), which probably requires mocking of $dispatchResult or even something else.
If you want more detailed answer, please provide more code samples of your classes and methods.
I am beginning to write tests using Laravel 4.2, PHPUnit, and Mockery.
How can I assert that a Mock Facade was called/run by the Controller that I am testing?
For example, I've got the following Mockery for Queue. My controller doesn't return anything that has to do with the Queue::push(), so is there a way I can assert that it triggered Queue::push()?
Queue::shouldReceive('push')->once();
Queue::shouldReceive('connected')->once();
If I comment out the Queue::push() in my controller, the Queue::shouldReceive throws an exception:
Mockery\Exception\InvalidCountException: Method push() from Mockery_0_Illuminate_Queue_QueueManager should be called exactly 1
times but called 0 times.
Is the only way to assert that the Mock Facade ran is to do something like the following?
$queuepushran = false;
try {
Queue::shouldReceive('push')->once();
Queue::shouldReceive('connected')->once();
$queuepushran = true;
} catch (\Mockery\Exception\InvalidCountException $e) {
//Queue threw an exception
$queuepushran = false;
}
$this->assertTrue($queuepushran);
You're on the right track with using mock objects and expectations to test this, but this isn't quite how mocks should be used. Your code snippet seems to be missing actually calling the controller, so it's hard to tell exactly what's going on, but I'll just run over a full example.
You want to test that when your controller method runs, the Queue facade's push() and connected() methods are called once each. What we want to do is use Laravel's cool facade functionality to replace the real QueueManager class behind the Queue facade with a mock one, that way when the controller calls Queue::push(..), it will be calling our mock object instead.
So, lets start building the test:
public function testSomeControllerMethodCallsTheQueue()
{
Queue::shouldReceive('push')->once();
Queue::shouldReceive('connected')->once();
}
We've now swapped out the real QueueManager for our mock one, and set up how we expect them to be called. Since each test should only be doing one thing, we've now done all we need to do to 'arrange' the test.
Next, we need to 'act' by calling the controller.
public function testSomeControllerMethodCallsTheQueue()
{
Queue::shouldReceive('push')->once();
Queue::shouldReceive('connected')->once();
$this->action('GET', 'YourController#method');
}
The last step is to 'assert', but we don't actually need to do anything. If our Mockery expectations are met, the test will complete and go green. If our Mockery expectations are not met, an exception will be thrown and the test will go red. The exceptions that Mockery throws are intended to be what fails your test, you don't need to do assertions like when comparing the return value or response with what you expect -- Mockery handles all that.