Mocking in controller tests with Laravel model binding - php

I'm using model binding within my routes to pass models into my controller actions and would like to be able to write tests. It would be preferable if it wasn't required for the test to hit the database.
The model is bound using the username in this example, and then used in the definition of the routes.
// routes.php
Route::model('user', function($value, $route)
{
return User::whereUsername($value)->firstOrFail();
});
Route::get('users/{user}', 'UsersController#show');
In my controller the bound user is passed to the action.
// UsersController.php
function show(User $user)
{
return View::make('users.show', compact('user');
}
Now, in my tests I'm attempting to mock the User.
// UsersControllerTest.php
public function setUp()
{
parent::setUp();
$this->mock = Mockery::mock('Eloquent', 'User');
$this->app->instance('User', $this->mock);
}
public function testShowPage()
{
$this->mock->shouldReceive('whereSlug')->once()->andReturn($this->mock);
$this->action('GET', 'UsersController#show');
$this->assertResponseOk();
$this->assertViewHas('user');
}
When running this test, I get the following error:
ErrorException: Argument 1 passed to UsersController::show() must be an instance of User, instance of Illuminate\Database\Eloquent\Builder given
I'd also like to be able to use return User::firstByAttribtues($value); but Mockery won't let me mock a protected method - is there any way I can get around this?

I had to dig thru Mockery's source code to find this, but have you looked at shouldAllowMockingProtectedMethods ?
Ie, to mock class foo and allow protected methods to be mocked:
$bar = \Mockery::mock('foo')->shouldAllowMockingProtectedMethods();
// now set your expectations up
and then keep going from there.

Not sure why you're not getting an error like unexpected method "firstOrFail" called. But, at first glance, I think the problem is that your model route defined in routes.php is also calling the firstOrFail method.
So, your test should look something like this:
public function testShowPage()
{
$stubQuery = \Mockery::mock('Illuminate\Database\Eloquent\Builder');
$this->mock->shouldReceive('whereSlug')->once()->andReturn($stubQuery);
$stubQuery->shouldReceive('firstOrFail')->andReturn($this->mock);
$this->action('GET', 'UsersController#show');
$this->assertResponseOk();
$this->assertViewHas('user');
}

Related

Skip authorization while functional testing a Laravel application

Is there a built-in way to skip authorization completely while testing the controllers?
Sample controller:
public function changePassword(Request $request, LdapInterface $ldap)
{
$this->authorize('change-password');
$this->validate($request, [
'pass' => 'min:8|confirmed|weakpass|required',
]);
$success = $ldap->updatePassword($request->get('pass'));
$message = $success ?
'Your e-mail password has been successfully changed' :
'An error occured while trying to change your alumni e-mail password.';
return response()->json(['message' => $message]);
}
I want to skip change-password rule, which is defined inside the AuthServiceProvider like:
public function boot(GateContract $gate)
{
$gate->define('change-password', function ($user) {
// Some complex logic here
});
}
I don't want to add smt. like if (env('APP_ENV') == 'testing') return; inside the code.
Actually there is a built-in way. You can add a "before" callback to be called before actual authorization check and bypass the check simply by returning true:
\Gate::before(function () {
return true;
});
You should add this snippet to either the setUp() method of your test or every test method that you want to bybass the authorization.
I'm not aware of one, but you could move that check to a dedicated middleware and use the withoutMiddleware trait to disable it in tests.
Or you could mock the application's gate instance using Mockery. Mockery is well documented so I'd suggest reading the docs for more details, but setting it up would look something like this:
$mock = Mockery::mock('Illuminate\Contracts\Auth\Access\Gate');
$mock->shouldReceive('authorize')->with('change-password')->once()->andReturn(true);
$this->app->instance('Illuminate\Contracts\Auth\Access\Gate', $mock);
This sets up a mock of the gate contract, sets up what it expects to receive and how it should respond, and then injects it into the application.
From laravel documentation :
When testing your application, you may find it convenient to disable
middleware for some of your tests. This will allow you to test your
routes and controller in isolation from any middleware concerns.
Laravel includes a simple WithoutMiddleware trait that you can use to
automatically disable all middleware for the test class:
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use WithoutMiddleware;
//
}
Or you can use withoutMiddleware() method in you test method like this :
public function testBasicExample()
{
$this->withoutMiddleware();
$this->visit('/')
->see('Laravel 5');
}
Ps : since Laravel 5.1
In my tests I add a method I can call at the beginning of a test to disable authorization only for that test.
Add to your base test class
public function withoutAuthorization()
{
\Gate::before(function () {
return true;
});
return $this;
}
And then in a test you can call it:
public function testSomeThing()
{
$this->withoutAuthorization();
// any gates will be bypassed
$this->get('/my-protected-endpoint');
}
It is much simpler.
Just take it out of the authorization required section in your routes file.
In this example I needed the AssignmentsController to work without Authentication, so I just moved it from the jwt.auth group up:
Route::post('v1/auth/login', 'Api\AuthController#authenticate');
Route::post('v1/auth/sendpassword', 'Api\AuthController#sendPassword');
Route::get('v1/assignments', 'Api\AssignmentsController#getAll');
Route::group(['middleware' => 'jwt.auth'], function() {
Route::post('v1/auth/logout', 'Api\AuthController#logout');
Route::post('v1/shipments', 'Api\ShipmentController#getShipments');
Route::post('v1/assignments/{id}/transfer', 'Api\AssignmentsController#saveAssignment');
Route::get('v1/shipments/assignments/{assignmentId}', 'Api\AssignmentsController#getDetail');
Route::post('v1/shipments/assignments/{id}/upload', 'Api\AssignmentsController#uploadFile');
});

Laravel attach returns undefined method for belongsToMany relationship

I would like to create a question which has many surveys. In the questions Model:
public function surveys()
{
return $this->belongsToMany(Survey::class, 'survey__surveyquestions');
}
And in the controller when saving a new question:
private $questions;
public function __construct(QuestionsRepository $questions)
{
parent::__construct();
$this->questions = $questions;
}
public function store(Request $request)
{
$this->questions->create($request->all());
$this->questions->surveys()->attach($request->surveys);
return redirect()->route('admin.survey.questions.index')
->withSuccess(trans('core::core.messages.resource created', ['name' => trans('survey::questions.title.questions')]));
}
But I get the following error when it gets to the attach line:
(1/1) FatalErrorException Call to undefined method
Modules\Survey\Repositories\Eloquent\EloquentQuestionsRepository::surveys()
I notice the error mentions EloquentQuestionsRepository but I have added no methods in there so it's just an empty class:
class EloquentQuestionsRepository extends EloquentBaseRepository implements QuestionsRepository
{
}
QuestionRepository:
interface QuestionsRepository extends BaseRepository
{
}
As explained in the response to the main post - the constructor resolves the QuestionsRepository to instance of EloquentQuestionsRepository, which by the look of it is not what the store method needs.
What I would probably do is to make call to create method directly on the model and remove constructor all together - that is unless you need the instance of QuestionsRepository anywhere else in your controller:
public function store(Request $request)
{
$question = Question::create($request->all());
$question->surveys()->attach($request->surveys);
...
}
Also - I'm not sure passing $request->all() is the best thing to do - I'd probably use $request->only(...) or $request->all(...) specifying which items you want to get from the request rather than passing everything from the request to the create method.
On the other note - you could also use Form Request, which would validate data for your before passing it to the store method.
https://laravel.com/docs/5.5/validation#form-request-validation

Passing variable from middleware to controllers __construct to prevent repeating myself

I'm doing an existence check within a middleware, by checking a route-parameter.
If the check succeeds, I'm attaching it's model to the request to make it available throughout the rest of the request-cycle, application.
// App\Http\Middleware\CheckForExistence.php:
...
public function handle($request, Closure $next)
{
// some checks...
// success
$request->attributes->add([
'company' => $someModel
]);
}
I now have a controller which 'needs' this information in a couple of methods. So my thought was to add it to the construct of the controller and add it as a protected var in the whole controller:
// App\Http\Controllers\MyController.php
<?php
use Illuminate\Http\Request;
class MyController extends Controller
{
protected $company;
public function __construct(Request $request)
{
$this->company = $request->attributes->get('company');
}
public function index()
{
dd($this->company); // returns null
}
}
This controllers index() returns null instead of the give model.
If I change the index() method to:
public function index(Request $request)
{
return $request->attributes->get('company');
}
This returns the model; as expected.
Why is this happening? It looks like the middleware is not run when the controller is constructed.... Is there a way to circumvent it?
Or am I missing the obvious here.....
I could off course repeat myself in each method; but that is not very DRY ;)
You can't access the session or authenticated user in your controller's constructor because the middleware has not run yet, So you can do it like this :
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->company = $request->attributes->get('company');
return $next($request);
});
}
For reasons currently unclear to me, the controller object is constructed before the request changes are reflected in the request object. In short the request is not considered properly constructed when a controller is constructed. This post seems to imply that.
There's two ways to work around this (if for a second we ignore what you're trying to do).
Use request dependency injection
public function index(Request $request)
{
$compary = $request->attributes->get('company');
}
This is not really WET because you're just swapping $this->company with $request->attributes->get('company') it's just a refactor. You should be injecting the request in the controller action anyway and if you don't want to do that you can use the request() helper.
Use a callback middleware in the constructor (Maraboc's answer explains how)
Now if you want a more case specific solution though you can use case specific dependency injection:
If you need to bind a model to a specific route parameter you can use route model binding and add the following in your RouteServiceProvider (or any provider).
Route::bind("companyAsARouteVarName", function () {
// this is why more details in the question are invaluable. I don't know if this is the right way for you.
//checks
// success
return $someModel;
});
Then you will register your route as:
Route::get("/something/{companyAsARouteVarName}", "SomeController#index");
and your controller will be:
public function index(Company $companyAsARouteVarName) {
//Magic
}
Controller constructor will be initialized before middleware execution.
You can get data from Injected $request object in controller functions.

Laravel singleton not working across controller/ViewComposer

In Laravel, I have a class that I would like to make available to the service controller, make some changes to in the controller action, and then render out with a ViewComposer.
I have done this several times before without issue, but for some reason this time my usual approach is not working - clearly I'm doing something different, and I'm beginning to suspect I've fundamentally misunderstood an aspect of what I am doing.
I have a ServiceProvider with this register() method:
public function register()
{
$this->app->singleton(HelperTest::class, function ($app) {
$pb = new HelperTest();
$pb->test = "jokes on you batman";
return $pb;
});
}
Then in my controller I'm doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
$this->helper->test = "hahah";
}
And then I have a viewcomposer doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
}
public function compose(View $view)
{
$view->with('output', $this->helper->test);
}
When I call {{ $output }} in the blade view, I expect to see hahah, but instead I get jokes on you batman.
My debugging has shown that all three of these methods are definitely being called. It looks to me like the ViewComposer is for some reason instantiating its own, fresh instance of the class. What am I doing wrong?
Thanks!
Execute php artisan optimize on your console, this will generate an optimized class loader for your application, then check if you can find your class HelperTest registered in services.php inside boostrap/cache. Until HelperTest is not registered there, Laravel IoC can't resolve your class.

Trying to swap a model in my controller test with IoC Container, but object not swapper

I'm trying to use IoC Container to swap out my Question model when testing. Although I've created a mock model, and using App::instance() to attempt to swap the dependency during my test, I can see from var_dump that it isn't working. What is wrong with my code?
<?php
class QuestionsControllerTest extends TestCase {
protected $mock;
public function __construct()
{
// This is how Net tuts tutorial instructed, but
// I got Eloquent not found errors
// $this->mock = Mockery::mock('Eloquent', 'Question');
// so I tried this instead, and it created the mock
$this->mock = Mockery::mock('App\Question');
}
public function tearDown()
{
Mockery::close();
}
public function testQuestionIndex()
{
// var_dump(get_class($this->mock)); exit; // outputs: Mockery_0_App_Question
// var_dump(get_class($this->app)); exit; // outputs: Illuminate\Foundation\Application
$this->mock
->shouldReceive('latest')
->once()
->andReturnSelf();
$this->mock
->shouldReceive('get') //EDIT: should be get
->once()
->andReturn('foo');
$this->app->instance('App\Question', $this->mock);
// dispatch route
$response = $this->call('GET', 'questions');
$this->assertEquals(200, $response->getStatusCode());
}
}
So far so good? Below is my QuestionsController:
class QuestionsController extends Controller {
protected $question;
public function index(Question $question)
{
// var_dump(get_class($question)); exit; // Outputs App\Question when testing too
$questions = $question
->latest()
->get();
return view('questions.index', compact('questions'));
}
...
So, without the object being swapped, it doesn't register anyway call to the methods:
Mockery\Exception\InvalidCountException: Method latest() from Mockery_0_App_Question should be called
exactly 1 times but called 0 times.
By the way, I've installed Mockery ~0.9, Laravel 5.0, and PHPUnit ~4.0. Would really really appreciate any help on this.
I think you need to specify the full namespace when using instance(). Laravel otherwise will assume the model is in the global namespace ('\Question').
This should work:
$this->app->instance('App\Question', $this->mock);
Now regarding the other problem, your mock. Since your view wants a collection why not just give it one? And if you don't want to test the view you can simply instantiate an empty collection and return that:
$this->mock
->shouldReceive('latest')
->once()
->andReturnSelf();
$this->mock
->shouldReceive('get')
->once()
->andReturn(new Illuminate\Database\Eloquent\Collection);
If you want you can also check if the view has correctly received the variable with:
$response = $this->call('GET', 'questions');
$this->assertViewHas('questions');
You are getting this error as you didn't define your mock completely.
you told your mock that it should expect latest to be called once, but you did not specify what latest should return. on the next line in your controller you call get.
Try the following code
$this->mock
->shouldReceive('latest')
->once()
->andReturnSelf();
$this->mock
->shouldReceive('get') //EDIT: should be get
->once()
->andReturn('foo');
$this->app->instance('Question', $this->mock);
Very good article about testing controllers in larvel http://code.tutsplus.com/tutorials/testing-laravel-controllers--net-31456

Categories