I am writing a unit test in Laravel 5.0 and in my request class I am using a different bag to show the validation error messages.
I am using this in my file:
/* ExampleRequest.php */
namespace App\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Support\Facades\Auth;
class ExampleRequest extends Request {
protected $errorBag = 'otherbag';
public function rules(){
return [
'my_field' => 'required'
];
}
}
In my test file, I am testing using this:
/* ExampleTest.php */
class ExampleTest extends TestCase {
public function testPostWithoutData(){
$response = $this->call('POST', 'url/to/post',[
'my_field' => ''
]);
$this->assertSessionHasErrors('my_field');
}
}
If I run the tests, it can't get the right assert and return this problem:
Session missing error: my_field
Failed asserting that false is true.
If I take out the $errorBag attribute from the request file, I have no problems.
I can give more details as needed.
You can get an alternate bag from the session store like this:
$myBag = $this->app('session_store')->getBag('otherBag');
$this->assertTrue($myBag->any());
However, Laravel does not use an alternate bag by default, so I'm assuming you're doing something in your code to register your App\Request::$errorBag with the session handler.
I don't know if you are setting your session elsewhere but I guess you may do something like:
$this->session(['foo' => 'bar']);
Before you can assert something in session. See testing helpers section for Laravel 5.0
Related
I am writing tests for my Laravel project. Right now I am testing the authentication code like login, logout, reset password and so on.
Sadly, my test is failing because there is no notification send. I have mocked the notifications but assertSendTo always fails with the reason The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent..
However, when actual requesting a reset password email (not in the test, as a normal user on my website) I indeed do get an reset password email. So, it is functional and working but not in my test. How can this be? The .evn is also correct, I have set my mail host to mailtrap.io and I also receive this email... This is the best proof I can give you.
Here is my test:
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Tests\TestCase;
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
Notification::fake();
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
$response->assertStatus(200);
$response->assertLocation(route('home'));
//$this->expectsNotification($user, ResetPassword::class);
Notification::assertSentTo($user, ResetPassword::class);
}
}
Any ideas why the test is not working or whats wrong with it?
Whats also very strange is the fact that the response code 200 is indicating that everything succeeded without any problem.
This is the error I get when executing the test with assertSentTo
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_not_logged_in_user_can_request_a_new_password
The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent.
Failed asserting that false is true.
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Testing/Fakes/NotificationFake.php:68
/MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:261
MyWebsiteProject/tests/Feature/Auth/LoggedIn/ForgotPassword/AuthTest.php:35
And this is the error I get when executing it with expectsNotification
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_logged_in_user_can_request_a_new_password
The following expected notification were not dispatched: [Illuminate\Auth\Notifications\ResetPassword]
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php:281
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153
Kind regards and thank you!
My app was initially created in L5 when the default User.php fully qualified class name was
App\User.php but, according to L8 new pattern, I've duplicated it (to avoid refactoring.... my fault) to App\Models\User.php.
https://twitter.com/taylorotwell/status/1296556354593792000
This partial misalignment, though, left some of my tests behind, expecially the ones involving fake-notifications in which UserFactory kept dispatching (via NotificationFake.php) to App\User.php instead of App\Models\User.php and determining the assertSentTo to fail.
TLDR
make sure that config/auth.php (param providers.users.model) and UserFactory.php rely on the same model.
Expects does just that "expect that something is going to happen", in your case you are expecting it after the fact. The latter is the use case for an assertion. Something like the following is what you need.
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
// Expect that something is going to happen
$this->expectsNotification($user, ResetPassword::class);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
// Assertion that something has happened
$response->assertStatus(200);
$response->assertLocation(route('home'));
}
}
In case non of the above solutions works for you like in my case, try adding this line at the top of your test function;
$this->withoutExceptionHandling();
This will give you a good error message as to where the problem might be coming from. In my case, I was using the wrong route.
I'm trying to write some tests for my forms in order to confirm the validators retrieve the expected errors when required.
The form only has 3 fields: name, discount and expiration and the validator looks like this:
$this->validate($request, [
'name' => 'required',
'discount' => 'required|numeric|between:1,100',
'expiration' => 'required|date_format:d/m/Y',
]);
That works fine both when submitting the form and when running the tests with phpunit using the following code:
/**
* Discount must be numeric check
*/
$response = $this->post(route('offer.create'), [
'name' => $faker->sentence(4),
'discount' => 'asdasd',
'expiration' => $faker->dateTimeBetween('+1 days', '+5 months')
]);
// Check errors returned
$response->assertSessionHasErrors(['discount']);
Since discount is not numeric it throws the expected error and everybody is happy.
Now, if I want to add a new rule to make sure that the expiration is equal or greater to today I add the after:yesterdayrule leaving the validator like:
$this->validate($request, [
'name' => 'required',
'discount' => 'required|numeric|between:1,100',
'expiration' => 'required|date_format:d/m/Y|after:yesterday',
]);
That works fine when submitting the form. I get the error saying the discount is not numeric, but when testing with phpunit it doesn't get the error as expected:
1) Tests\Feature\CreateSpecialOfferTest::testCreateSpecialOffer
Session missing error: expiration
Failed asserting that false is true.
Why adding this new validation rule to expirationgenerates a false validation in discount? Is this a bug in the validator or am I missing something?
Also:
1 - is there a better way to test form validators?
2 - is there an assert that is the opposite of assertSessionHasErrors() to check a certain error is NOT been thrown?
If you see this kind of errors in PHPUnit: Failed asserting that false is true., you can add the 'disableExceptionHandling' function to tests/TestCase.php:
<?php
namespace Tests;
use Exception;
use App\Exceptions\Handler;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function disableExceptionHandling()
{
// Disable Laravel's default exception handling
// and allow exceptions to bubble up the stack
$this->app->instance(ExceptionHandler::class, new class extends Handler {
public function __construct() {}
public function report(Exception $exception) {}
public function render($request, Exception $exception)
{
throw $exception;
}
});
}
}
In your test you call it like this:
<?php
/** #test */
public function your_test_function()
{
$this->disableExceptionHandling();
}
Now, the full output of the error and stacktrace will be shown in the PHPUnit console.
I'm trying to create unit tests to test some specific classes. I use app()->make() to instantiate the classes to test. So actually, no HTTP requests are needed.
However, some of the tested functions need information from the routing parameters so they'll make calls e.g. request()->route()->parameter('info'), and this throws an exception:
Call to a member function parameter() on null.
I've played around a lot and tried something like:
request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);
request()->route(['info' => 5]);
request()->initialize([], [], ['info' => 5], [], [], [], null);
but none of them worked...
How could I manually initialize the router and feed some routing parameters to it? Or simply make request()->route()->parameter() available?
Update
#Loek: You didn't understand me. Basically, I'm doing:
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
No "requests" involved. The request()->route()->parameter() call is actually located in a service provider in my real code. This test case is specifically used to test that service provider. There isn't a route which will print the returning value from the methods in that provider.
I assume you need to simulate a request without actually dispatching it. With a simulated request in place, you want to probe it for parameter values and develop your testcase.
There's an undocumented way to do this. You'll be surprised!
The problem
As you already know, Laravel's Illuminate\Http\Request class builds upon Symfony\Component\HttpFoundation\Request. The upstream class does not allow you to setup a request URI manually in a setRequestUri() way. It figures it out based on the actual request headers. No other way around.
OK, enough with the chatter. Let's try to simulate a request:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
As you mentioned yourself, you'll get a:
Error: Call to a member function parameter() on null
We need a Route
Why is that? Why route() returns null?
Have a look at its implementation as well as the implementation of its companion method; getRouteResolver(). The getRouteResolver() method returns an empty closure, then route() calls it and so the $route variable will be null. Then it gets returned and thus... the error.
In a real HTTP request context, Laravel sets up its route resolver, so you won't get such errors. Now that you're simulating the request, you need to set up that by yourself. Let's see how.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
See another example of creating Routes from Laravel's own RouteCollection class.
Empty parameters bag
So, now you won't get that error because you actually have a route with the request object bound to it. But it won't work yet. If we run phpunit at this point, we'll get a null in the face! If you do a dd($request->route()) you'll see that even though it has the info parameter name set up, its parameters array is empty:
Illuminate\Routing\Route {#250
#uri: "testing/{info}"
#methods: array:2 [
0 => "GET"
1 => "HEAD"
]
#action: array:1 [
"uses" => null
]
#controller: null
#defaults: []
#wheres: []
#parameters: [] <===================== HERE
#parameterNames: array:1 [
0 => "info"
]
#compiled: Symfony\Component\Routing\CompiledRoute {#252
-variables: array:1 [
0 => "info"
]
-tokens: array:2 [
0 => array:4 [
0 => "variable"
1 => "/"
2 => "[^/]++"
3 => "info"
]
1 => array:2 [
0 => "text"
1 => "/testing"
]
]
-staticPrefix: "/testing"
-regex: "#^/testing/(?P<info>[^/]++)$#s"
-pathVariables: array:1 [
0 => "info"
]
-hostVariables: []
-hostRegex: null
-hostTokens: []
}
#router: null
#container: null
}
So passing that ['info' => 5] to Request constructor has no effect whatsoever. Let's have a look at the Route class and see how its $parameters property is getting populated.
When we bind the request object to the route, the $parameters property gets populated by a subsequent call to the bindParameters() method which in turn calls bindPathParameters() to figure out path-specific parameters (we don't have a host parameter in this case).
That method matches request's decoded path against a regex of Symfony's Symfony\Component\Routing\CompiledRoute (You can see that regex in the above dump as well) and returns the matches which are path parameters. It will be empty if the path doesn't match the pattern (which is our case).
/**
* Get the parameter matches for the path portion of the URI.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function bindPathParameters(Request $request)
{
preg_match($this->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
return $matches;
}
The problem is that when there's no actual request, that $request->decodedPath() returns / which does not match the pattern. So the parameters bag will be empty, no matter what.
Spoofing the request URI
If you follow that decodedPath() method on the Request class, you'll go deep through a couple of methods which will finally return a value from prepareRequestUri() of Symfony\Component\HttpFoundation\Request. There, exactly in that method, you'll find the answer to your question.
It's figuring out the request URI by probing a bunch of HTTP headers. It first checks for X_ORIGINAL_URL, then X_REWRITE_URL, then a few others and finally for the REQUEST_URI header. You can set either of these headers to actually spoof the request URI and achieve minimum simulation of a http request. Let's see.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], [], [], [], ['REQUEST_URI' => 'testing/5']);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
To your surprise, it prints out 5; the value of info parameter.
Cleanup
You might want to extract the functionality to a helper simulateRequest() method, or a SimulatesRequests trait which can be used across your test cases.
Mocking
Even if it was absolutely impossible to spoof the request URI like the approach above, you could partially mock the request class and set your expected request URI. Something along the lines of:
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$requestMock = Mockery::mock(Request::class)
->makePartial()
->shouldReceive('path')
->once()
->andReturn('testing/5');
app()->instance('request', $requestMock->getMock());
$request = request();
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
This prints out 5 as well.
I ran into this problem today using Laravel7 here is how I solved it, hope it helps somebody
I'm writing unit tests for a middleware, it needs to check for some route parameters, so what I'm doing is creating a fixed request to pass it to the middleware
$request = Request::create('/api/company/{company}', 'GET');
$request->setRouteResolver(function() use ($company) {
$stub = $this->createStub(Route::class);
$stub->expects($this->any())->method('hasParameter')->with('company')->willReturn(true);
$stub->expects($this->any())->method('parameter')->with('company')->willReturn($company->id); // not $adminUser's company
return $stub;
});
Since route is implemented as a closure, you can access a route parameter directly in the route, without explicitly calling parameter('info'). These two calls returns the same:
$info = $request->route()->parameter('info');
$info = $request->route('info');
The second way, makes mocking the 'info' parameter very easy:
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
Of course to exploit this method in your tests, you should inject the Request object in your class under test, instead of using the Laravel global request object through the request() method.
Using the Laravel phpunit wrapper, you can let your test class extend TestCase and use the visit() function.
If you want to be stricter (which in unit testing is probably a good thing), this method isn't really recommended.
class UserTest extends TestCase
{
/**
* A basic test example.
*
* #return void
*/
public function testExample()
{
// This is readable but there's a lot of under-the-hood magic
$this->visit('/home')
->see('Welcome')
->seePageIs('/home');
// You can still be explicit and use phpunit functions
$this->assertTrue(true);
}
}
In Symfony, when a user attempts to access a route which is forbidden for that specific user (according to the user roles), a page with response code 403 will be returned.
So the user can still see that there is a valid route there.
I would like to overwrite this behavior by replacing the status code 403 with 404, so the user will just see that there is no valid route when she/he is not allowed to access that resource.
How can I accomplish that?
This is doable, however almost undocumented. I'm aware of two ways but there might be even more:
Using access_denied_url configuration option. See security config reference. With this option you can set URL where the user is redirected when the user in unauthorized (I think it should work also with route name). See a similar question: Symfony2 Redirection for unauthorised page with access_denied_url
There're also "Entry Points" as mentioned in The Firewall and Authorization. However, no examples, no explanation how to use it.
I looks like this option expects a service name as can be seen in security config reference (search for entry_point option).
One possible solution, as partially explained here, can be the following:
1) Defining a new service controller in services.yml
exception_controller:
class: Path\To\MyBundle\Controller\MyExceptionController
arguments: ['#twig', '%kernel.debug%']
2) Creating the new class which overrides Symfony\Bundle\TwigBundle\Controller\ExceptionController:
namespace Path\To\MyBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MyExceptionController extends ExceptionController
{
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
$currentContent = $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1));
$showException = $request->attributes->get('showException', $this->debug); // As opposed to an additional parameter, this maintains BC
$code = $exception->getStatusCode();
if ($code == 403) {
$code = 404;
// other customizations ...
}
return new Response($this->twig->render(
(string) $this->findTemplate($request, $request->getRequestFormat(), $code, $showException),
array(
'status_code' => $code,
'status_text' => isset(Response::$statusTexts[$code]) ? Response::$statusTexts[$code] : '',
'exception' => $exception,
'logger' => $logger,
'currentContent' => $currentContent,
)
));
}
}
3) Setting the following in config.yml under twig:
twig:
exception_controller: 'exception_controller:showAction'
Even though my original goal was to prevent such an exception to be thrown at all with that code.
Another solution can be overwriting the AccessListener service of the Symfony Security component.
The generic procedure about how to override a service of a bundle is documented here. The following is the concrete example about this particular situation.
First of all let's create the class which overrides the AccessListener class:
<?php
namespace Path\To\My\Bundle\Services;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class OverrideAccessListener extends AccessListener
{
public function handle(GetResponseEvent $event)
{
try {
parent::handle($event);
} catch (AccessDeniedException $e) {
$request = $event->getRequest();
$message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo());
if ($referer = $request->headers->get('referer')) {
$message .= sprintf(' (from "%s")', $referer);
}
throw new NotFoundHttpException($message);
}
}
}
then we need to create a Compiler Pass in order to change the class attribute of the original service with the new class:
<?php
namespace Path\To\My\Bundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.access_listener');
$definition->setClass('Path\To\My\Bundle\Services\OverrideAccessListener');
}
}
finally we need to register the Compiler Pass in the build method of the bundle:
<?php
namespace Path\To\My\Bundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Path\To\My\Bundle\DependencyInjection\Compiler\OverrideServiceCompilerPass;
class MyBundleName extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideServiceCompilerPass());
}
}
Finally I found a simpler solution: using an access denied handler.
Unfortunately there is no much documentation about how to create an access denied handler, but it is very simple.
First create a class that implements the AccessDeniedHandlerInterface and set it as a service (for example naming it my_access_denied_handler_service).
In the handle method a Response should be created and returned (in my case I wanted a 404 response).
Then in the security.yml configuration file we have to set the access_denied_handler under the firewall:
...
firewalls:
my_firewall:
...
access_denied_handler: my_access_denied_handler_service
...
...
Basically I have to write tests for many Laravel Controllers most of which are CRUD (read, store, update) and most of the logic is placed inside those(Inherited code, not mine).
What I need to do is automate the testing from a User's perspective. So I need to hit all the endpoints and test against a real database and check if everything turns out well.
I have almost no experience in testing, but from what I gather controllers should be tested with integration / acceptance tests. Now I did fine with testing Read methods by extending Laravel's TestCase, here is one example :
class SongsTest extends TestCase
{
public function testBasicIndex()
{
$arguments = [];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
/**
* #dataProvider providerSearchQuery
*/
public function testSearchIndex($query)
{
$arguments = ['srquery' => $query];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
public function providerSearchQuery()
{
return array(
array('a'),
array('as%+='),
array('test?Someqsdag8hj$%$')
);
}
public function testGetSongsById()
{
$id = 1;
$response = $this->call('GET', '/songs/' . $id);
$this->assertContains($response->getStatusCode(), [200, 404]);
$this->seeJson();
if($response->getStatusCode() == 404)
{
$content = json_decode($response->getContent());
$this->assertContains($content->message[0], ['Song not found', 'Item not active']);
}
}
}
These tests hit the endpoints and check if the response is 200 and the format is JSON and few other things. These work fine.
What I have problem with is :
Let's say for example we have a UserController, and a method that creates users. After that, said user should be used in TokensController to create a token that should be somehow remembered and used in future tests with token protected requests.
My question :
How do I automate : tests of UserController's store method by creating a real user in a testing database(without mocking), tests of TokensController's store method by using that user's email, testing other controllers with the created token and deleting that once the test is done, so it can be performed once again.
I just cannot conceptualize all that since I haven't really done much testing.
This is an example to use token and user's data for testing -
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class PostTest extends TestCase
{
use WithoutMiddleware;
public $token = "lrSFMf0DpqRAh4BXTXWHp5VgFTq4CqA68qY3jG2CqvcpNTT6m0y9Qs6OdpSn";
/*
A browser that receives a 302 response code HAS to redirect which means it will take the URL in the response and submit a new request. The result you see in your browser is the redirected page.
Unit testing does not redirect. Your unit test is only doing what you direct it to do. If your unit test should test for the redirect then you evaluate the response and the correct assertion is 302 and not 200.
*/
public function testExample()
{
$this->assertTrue(true);
}
public function testLogin()
{
$this->visit('/')
->type('abc#gmail.com', 'email')
->type('123456', 'password')
->press('Login') // type submit - value / button - lable
->seePageIs('/Wall'); // for redirect url
}
public function testFavourite()
{
$this->testLogin();
$request = [
'user_id' => '172',
'token' => $this->token,
'post_id' => '500'
];
$response = $this->call('POST', '/DoFavoriteDisc',$request);
$this->assertEquals(200, $response->getStatusCode());
}
}
Hope this will help you.