Hi I'm trying to create a test for a laravel controller not dependent on the view. I have the following method in my controller class:
public function store(Request $request)
{
$this->validate($request,[
'name'=>'required',
'login'=>'required',
'password'=>'required'
]);
//TODO: Store to database
return redirect('usuario/nuevo');
}
And I found the following code to test whether the request had any errors:
public function testStore()
{
$response=$this->call('GET','usuario.store');
$this->assertSessionHasErrors();
}
As it is this test should pass since I'm sending a request without the required field filled out however PhpUnit returns the following message:
Session missing key: errors
Failed asserting that false is true.
The only way I can make it work is to try to "see" the error message on the response page by making the test like:
public function testStore()
{
$this->visit('/usuario/nuevo')
->press('Crear')
->see('Whoops');
}
This doesn't work for me for two reasons: 1)The test depends on the view to "press" the button and send the request(I wish to test this in a different test and keep this one strictly for the controller) and 2)The test depends on the 'Whoops' string to be present which is obviously bad practice.
I have found several answers claiming that the fist test code should work, but it simply doesn't.
Try it with session start and send empty parameters
$this->startSession();
$this->call('GET','usuario.store', array(
'name' => '',
'login' => '',
'password' => ''
));
$errors = session('errors');
$this->assertSessionHasErrors();
$this->assertEquals($errors->get('name')[0],""your custom error message);// only for checking exact message
Your problem is you are not testing what you think you are testing.
To execute the controller's store() you should use a POST method and not GET unless your routes are really weird.
Related
I have a somewhat simple authentication system using ReactTS as a frontend and Laravel as a backend. What I am looking to do is send errors from the backend to the frontend when I redirect a user. This is the code I currently have:
return redirect('/account')->withErrors('Error here');
I have also tried:
return redirect('/account')->with('message', 'Error here');
But in my React frontend I use a flash manager to handle errors. However I do not know how to get this error or session data in the frontend. Code:
addFlash({ key: 'account', message: 'error here' });
This function just takes a message and shows it to the user with a coloured bar. My issue is I do not know how to get these errors I am redirecting with in the frontend.
As I understand your question you have separated backend and frontend and probably have an API that handles your requests. If this is the case and I understood it correctly then I'll tell you how I handled it and maybe it will help you as well.
In my case, I'm returning an HTTP response no matter if it is for errors or for success and I think that you should do the same, let me show you a simple example to explain it better for you.
Here's my controller (simplified for your concern):
class CreateEventRegistrationController
{
public function __invoke(Request $request)
{
/**
Here you validate your fields or something
specific logic for your project.
*/
$validator = Validator::make($request->all(), [
'firstName' => 'required',
'lastName' => 'required'
]);
/**
If your validation fails then throw an HttpResponseException
*/
if ($validator->fails()) {
throw new HttpResponseException(response()->json([
'errors' => $validator->errors()
]));
}
/**
Store something in database or whatever you need to do in your app
*/
/**
And at last return a successful response
after everything is working as expected.
*/
return response('Created successfully', Response::HTTP_CREATED);
}
And with this logic then I have the messages on my frontend part where I handle there the error messages or success ones.
Hope this helps you, if I couldn't understand your issue properly you can comment it more specifically so I can help you more.
When testing a route in Laravel I cannot seem to assert against both the returned response and the session. Is there any reason for this and should I instead be splitting the test into two?
Here's my test, simplified:
$response = $this->call('POST', route('some-route'), $data);
This passes:
$this->assertResponseStatus(302);
This doesn't pass:
$this
->assertResponseStatus(302)
->assertSessionHasErrors([
'application_status' => 'Application status has changed. Action not applied.'
]);
The test will throw up an error saying it can't assert against null.
I've tried moving the order of the tests round and also assigning the response and session to variables before asserting like so:
$response = $this->call('POST', route('admin.application.action.store'), $data);
$sessionErrors = session()->get('errors');
$this
->assertEquals(302, $response->status())
->assertTrue($sessionErrors->has('application_status'));
But still get the same issue. Error: Call to a member function assertTrue() on null
Any advice would be greatly appreciated. Thanks!
Assertions don't implement fluid interface. Run it as 2 sequential statements:
$this->assertResponseStatus(302);
$this->assertSessionHasErrors([
'application_status' => 'Application status has changed. Action not applied.'
]);
I defined a test which tests the creation of a user. The controller is set to redirect back to the same page on error (using validation through a generated App\Http\Requests\Request). This works correctly when manually clicking in a browser, but fails during a test. Instead of being redirected to:
http://localhost/account/create
The test redirects to (missing a slash):
http://localhostaccount/create
Neither of these urls are what I have setup in the .htaccess or in the $url variable in config/app.php. Which is (On OSX Yosemite):
http://~username/laravel_projects/projectname/public
I finally pinpointed the issue to have something to do with how the result of Request::root() is generated. Making a call to this outside of a test results in the expected value defined in .htaccess and $url. Inside the test it results in:
http://localhost
What configuration needs to change in order to get this function to return the correct value in both contexts?
I should also mention I made the painful upgrade from Laravel 4 to the current version 5.0.27.
****** UPDATE *******
I was able to figure out an acceptable solution/workaround to this issue!
In Laravel 5, FormRequests were introduced to help move validation logic out of controllers. Once a request is mapped to the controller, if a FormRequest (or just Request) is specified, this is executed before hitting the controller action.
This FormRequest by default handles the response if the validation fails. It attempts to construct a redirect based on the route you posted the form data to. In my case, possibly related to an error of mine updating from Laravel 4 to 5, this default redirect was being constructed incorrectly. The Laravel System code for handling the response looks like this:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
Notice how the returned redirect is NOT the same as calling Redirect::route('some_route'). You can override this response function by including use Response in your Request class.
After using Redirect::route() to create the redirect, the logic in my tests passed with the expected results. Here is my Request code that worked:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Request;
use Response;
class AccountRequest extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:50|email|unique:users',
'password' => 'required|min:6',
'password_confirmation' => 'required|same:password'
];
}
public function response(array $errors){
return \Redirect::route('account_create');
}
}
The important part is that I called Redirect::route instead of letting the default response code execute.
Override the response function in the FormRequest validation handler to force the redirect to be constructed with Redirect::route('named_route') instead of allowing the default redirect.
You need to change config/app.php file's url value. Default value is http://localhost
Doc from config/app.php
This URL is used by the console to properly generate URLs when using the Artisan command line tool. You should set this to the root of your application so that it is used when running Artisan tasks.
I know this isn't an exact answer to your question since it is not a configuration update that solves the problem. But I was struggling with a related problem and this seems to be the only post on the internet of someone dealing with something similar - I thought I'd put in my two cents for anyone that wants a different fix.
Please note that I'm using Laravel 4.2 at the moment, so this might have changed in Laravel 5 (although I doubt it).
You can specify the HTTP_HOST header when you're testing a controller using the function:
$response = $this->call($method, $uri, $parameters, $files, $server, $content);
To specify the header just provided the $server variable as an array like so:
array('HTTP_HOST' => 'testing.mydomain.com');
When I did the above, the value produced for my Request::root() was http://testing.mydomain.com.
Again, I know this isn't a configuration update to solve you're issue, but hopefully this can help someone struggling with a semi-related issue.
If you tried changine config/app.php and it did not help.
it is better to use $_ENV - global variable in phpunit.
say, you want Request::root() to return 'my.site'
but you cannot touch phpunit.xml
you can simply set an env param like so
$_ENV['APP_URL'] = 'my.site';
and call $this->refreshApplication(); in your unittest.
viola, your request()->root() is giving you my.site now.
Here is my test code:
public function testRegistrationFailsIfNameIsEmpty()
{
$this->flushSession();
$response = $this->call('POST', '/signup', ['fullname' => '']);
$this->assertSessionHasErrors('fullname'); // Passes, expected
$this->assertTrue($response->isRedirection()); // Passes, expected
$this->assertRedirectedTo('/signup'); // Fails, unexpected.
}
When I call that method, it's validating the input, and if the validation fails, it redirects me back to /signup to show the validation errors. I've manually tested this in the browser, and it works as expected.
However, when I run the above unit test, the last assertion fails, and it thinks I've been redirected to just / rather than /signup.
I have no idea why it's doing this. If I test that a redirect happened at all, the test passes because a redirect does happen, it just thinks the redirect is to / instead of /signup.
I've disabled all middleware so I know it's not something like guest middleware thinking I'm logged in when I'm not.
EDIT: Test Results:
There was 1 failure:
1) RegistrationTest::testRegistrationFailsIfNameIsEmpty
Failed asserting that two strings are equal.
--- Expected
+++ Actual
## ##
-'http://localhost/signup'
+'http://localhost'
Using request()->validate() will return you to the calling url if validation fails. When running unit tests, there is no calling page so you will be directed to home.
https://laravel.com/docs/5.5/validation#quick-writing-the-validation-logic
if validation fails, an exception will be thrown and the proper error
response will automatically be sent back to the user. In the case of a
traditional HTTP request, a redirect response will be generated
So you either need to test that your redirect is to home, or fudge the calling url. Though then you're just testing the validator rather than your code, which doesn't really add anything:
session()->setPreviousUrl('/calling/url');
https://www.neontsunami.com/posts/testing-the-redirect-url-in-laravel
Edit:
Actually, this can be useful when the test expects the redirect to go to a route with a named parameter.
Edit for Laravel 5.8:
The test should be called with:
$this->from('/calling/url')
->get('/some/route')
->assertRedirect('/somewhere/else');
I had this same issue when testing a Laravel app. It seems that when you use $this->call in the test and then your controller uses something like Redirect::back(), the redirect is sent to '/'. I believe this is because you weren't on any page when you made "$this->call" so the application is unsure where to redirect you, unless you are explicit about the redirect in the controller, i.e. redirect('somePage').
However when doing something like $this->actingAs($user)->visit('somePage')->press('someButton'), you are correctly sent back to the expected page when using Redirect::back(). Likely because the app knows what page you started on.
You have to set ->from(route('RouteName')) in order to make sure the redirection assertion is properly set
I recommend you to print Response object from controller with dd() when unit testing this single test. This will show you where exactly the redirect is made. Unit test engine just parses the Response object and I believe the problem is in the code rather in the Unit test.
Could you check the test hitting the redirect function in controller
you can put a die in function and find-out where is the error
It should be an issue in method.
then check it actually goes in to the redirect part
for example you want to test a request validation failure. And for example it suppose to redirect your user to a previous url with validation failed exception and some messages. Say, you want to update a customer name.
// --- making a post request ----
$name = 'a new name that is too long';
$route = route('customer.update');
$response = $this
->withSession(['_previous' => ['url' => 'https://google.com']])
->call('POST', $route, [
'_token' => csrf_token(),
'name' => $name,
]);
// --- unit test assertions ----
$response->assertStatus(302);
$response->assertRedirect('https://google.com');
$message = __('#lang_file.some error message text here.');
$expected = [
'default' => ['' => [$message]]
];
$actual = session_errors()->getBags();
$actual = json_encode($actual);
$actual = json_decode($actual, true);
$this->assertSame($expected, $actual);
a function that return session errors:
function session_errors(): ViewErrorBag
{
$key = config('session.keys.errors');
$errors = session()->get($key, app(ViewErrorBag::class));
return $errors;
}
I understand how to validate requests by type-hinting the class name in the controller method. However for Ajax requests, According to the documentation, I should validate data in the controller, because using a validator class will redirect rather than send a response.
The main part I'm looking at is this:
If the incoming request was an AJAX request, no redirect will be
generated. Instead, an HTTP response with a 422 status code will be
returned to the browser containing a JSON representation of the validation errors.
However, my controller is as follows:
public function update(App\Permission $permission, Request $request)
{
$this->validate($request, [
'permission_description' => 'required|string'
]);
...
}
And I can't for the life of me get it to respond with JSON. The documentation states that if it fails, it throws an Illuminate\Contracts\Validation\ValidationException exception, but I can't catch it.
Whenever it fails, it always redirects back to the edit page. Obviously I don't want this, I want the json response.
I have just tried "manually writing it out" with the whole $v = Validator::make($request->all(), ...); which does work, but what's the point in using the $this->validate() way if it doesn't work?
Does the $this->validate() method just not work with AJAX and I have to write it the long way each time? Am I doing something wrong?!
Below is what I've tried:
public function update(App\Permission $permission, UpdatePermissionRequest $request)
{
/** Redirects rather than returns JSON if the validation fails **/
}
----------------------------------
public function update(App\Permission $permission, Request $request)
{
$this->validate($request, [
'permission_description' => 'required|string'
]);
/** AND I've also tried: **/
try {
$this->validate($request, ['permission_description' => 'required|string']);
} catch (\Illuminate\Contracts\Validation\ValidationException $e {
echo $e; /** Echoing for debug reasons **/
exit;
}
...
/** Still redirects the browser, even if it is an AJAX request **/
}
-----------------------------------------
use Validator;
...
public function update(App\Permission $permission, Request $request)
{
$v = Validator::make($request->all(), [
'permission_description' => 'required|string'
]);
if($v->fails())
{
return response()->json(['reply' => false]);
}
/** Works **/
}
UPDATE
The documentation is incorrect. It states that the $this->validate() method throws a Illuminate\Contracts\Validation\ValidationException but it doesn't. It throws a Illuminate\Http\Exception\HttpResponseException exception.
Simply telling that you want json in the header should also fix this. Laravel checks if the request is ajax of if json is requested.
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
Solution:
Add header
Accept: application/json
Your statement that the docs say it is best to validate AJAX requests in the controller is simply incorrect.
If you scroll a bit further down from what you linked - you'll see this under the FormValidation section
If validation fails, a redirect response will be generated to send the
user back to their previous location. The errors will also be flashed
to the session so they are available for display. If the request was
an AJAX request, a HTTP response with a 422 status code will be
returned to the user including a JSON representation of the validation
errors.
In other words - there is no reason you cannot do this in a simple FormRequest and simply your code significantly. It will automatically handle the fact it is an AJAX call and return the appropriate HTTP responses.
I do this all the time in my L5 apps - it works flawlessly.
Ok, so it looks like there were 2 contributing factors.
The reason it was redirecting instead of responding with JSON is because the X-Requested-With field wasn't set on the AJAX request. This the AJAX wrapper for the application was setup to deal with Cross Domain requests, which seems to strip out the X-Requested-With field, which makes the ultimate request look non-ajax to the server - hence the redirection.
The reason why it wasn't catching the Illuminate\Contracts\Validation\ValidationException exception is because that exception is not thrown. If you open up Illuminate\Foundation\Validation\ValidatesRequest.php, and search for the function throwValidationException(), it actually throws a HttpResponseException instead. I attempted to catch the HttpResponseException and it was caught successfully - I assume the documentation is wrong.
The solution was to either remove the cross domain attributes on the ajax request, add the X-Requested-With header manually, as per the answer in this post. This would make the application see that it was an AJAX request.
And if you wanted to manually catch the exception, you need to catch the HttpResponseException, not the ValidationException.