Symfony project PHPunit coverage test
UserController
public function userEdit($id, Request $request)
{
$user = $this->userRepository->findOneByCode($id);
if (!$user) {
throw new Exception("User not found!");
}
$userForm = $this->createForm(UserForm::class, $user);
$userForm->handleRequest($request);
if ($userForm->isSubmitted() && $userForm->isValid()) {
$this->userService->save($user);
return $this->redirectToRoute('user_list');
}
return $this->render(
'user/user.html.twig', [
'form' => $userForm->createView(),
]
);
}
TestUserController
public function testUserEdit()
{
$client = static::createClient();
$crawler = $client->request('GET', '/user/test/edit');
$formData = array(
'username' => 'test',
'email' => 'test#test.nl',
'roles' => 'ROLE_ADMIN'
);
$this->assertEquals(
200,
$client->getResponse()->getStatusCode()
);
$form = $this->factory->create(UserForm::class);
$object = User::fromArray($formData);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
In the userEdit method we have a if loop. But When we run PHPunit coverage test the if loop is not executed. The other if loop for submit is also not covered.
What goes wrong and what can I do in order to cover the test ? Also is this the best solution for Symfony form test since I am new to PHPunit.
I noticed a few things in your code that seem wrong:
The userEdit method name should be userEditAction.
Select the form like this:
$form = $crawler->selectButton('submit')->form();
That 'submit' text is the label on the submit button (e.g. Save).
And then, after filling the fields:
$crawler = $client->submit($form);
You check if the submit was successful by asserting that the resulting HTML page contains expected element (e.g.):
$this->assertGreaterThan(0, $crawler->filter('h1')->count());
Btw. put: $client->followRedirects(true); after instantiating the Client.
Examples are from the official docs.
Regarding the some lines that were not covered by test: whenever you have if clause, you need to test for both conditions. In your case, first you probably have a valid user, instance of User and the other case should be that you pass there an invalid user (null or whatever else). That is usually accomplished by using #dataProvider annotation and method. The data provider method supplies sets of data to the test method. There can be more than one set, so another set contains invalid data to cover the other outcome of the if() clause.
This blog has great examples.
To cover the content of the if-conditions you have to fulfill the conditions new tests. To enter the first if for example you have to write a test where you mock the userRepository and make findOneByCode return null. Then the following if-condition will be executed and throw an exception. Finally you test for a thrown exception in the test.
For the other if-condition you proceed in a similar manner. Write a new test which is designed to fulfill the condition and test the code inside it.
Related
When you're using the app through the browser, you send a bad value, the system checks for errors in the form, and if something goes wrong (it does in this case), it redirects with a default error message written below the incriminated field.
This is the behaviour I am trying to assert with my test case, but I came accross an \InvalidArgumentException I was not expecting.
I am using the symfony/phpunit-bridge with phpunit/phpunit v8.5.23 and symfony/dom-crawler v5.3.7.
Here's a sample of what it looks like :
public function testPayloadNotRespectingFieldLimits(): void
{
$client = static::createClient();
/** #var SomeRepository $repo */
$repo = self::getContainer()->get(SomeRepository::class);
$countEntries = $repo->count([]);
$crawler = $client->request(
'GET',
'/route/to/form/add'
);
$this->assertResponseIsSuccessful(); // Goes ok.
$form = $crawler->filter('[type=submit]')->form(); // It does retrieve my form node.
// This is where it's not working.
$form->setValues([
'some[name]' => 'Someokvalue',
'some[color]' => 'SomeNOTOKValue', // It is a ChoiceType with limited values, where 'SomeNOTOKValue' does not belong. This is the line that throws an \InvalidArgumentException.
)];
// What I'd like to assert after this
$client->submit($form);
$this->assertResponseRedirects();
$this->assertEquals($countEntries, $repo->count([]));
}
Here's the exception message I get :
InvalidArgumentException: Input "some[color]" cannot take "SomeNOTOKValue" as a value (possible values: "red", "pink", "purple", "white").
vendor/symfony/dom-crawler/Field/ChoiceFormField.php:140
vendor/symfony/dom-crawler/FormFieldRegistry.php:113
vendor/symfony/dom-crawler/Form.php:75
The ColorChoiceType tested here is pretty standard :
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => ColorEnumType::getChoices(),
'multiple' => false,
)];
}
What I can do, is to wrap in a try-catch block, the line where it sets the wrong value. And it would indeed submit the form and proceed to the next assertion. The issue here is that the form was considered submitted and valid, it forced an appropriate value for the color field (the first choice of the enum set).
This is not what I get when I try this in my browser (cf. the intro).
// ...
/** #var SomeRepository $repo */
$repo = self::getContainer()->get(SomeRepository::class);
$countEntries = $repo->count([]); // Gives 0.
// ...
try {
$form->setValues([
'some[name]' => 'Someokvalue',
'some[color]' => 'SomeNOTOKValue',
]);
} catch (\InvalidArgumentException $e) {}
$client->submit($form); // Now it submits the form.
$this->assertResponseRedirects(); // Ok.
$this->assertEquals($countEntries, $repo->count([])); // Failed asserting that 1 matches expected 0. !!
How can I mimic the browser behaviour in my test case and make asserts on it ?
It seems that you can disable validation on the DomCrawler\Form component. Based on the official documentation here.
So doing this, now works as expected :
$form = $crawler->filter('[type=submit]')->form()->disableValidation();
$form->setValues([
'some[name]' => 'Someokvalue',
'some[color]' => 'SomeNOTOKValue',
];
$client->submit($form);
$this->assertEquals($entriesBefore, $repo->count([]); // Now passes.
So the title describes my problem pretty well I think, but let me explain why I want to do this as theremight be an other solution to my problem that I haven't thought about.
Let's say that I have a route specifying the class of the object it will patch:
Route::patch('{class}/{id}', array(
'as' => 'object.update',
function ($class, $id) {
$response = ...;
// here I want to call the update action of the right controller which will
// be named for instance CarController if $class is set to "car")
return $response;
}
));
This is something pretty easy to do with $app->make($controllerClass)->callAction($action, $parameters); but doing it this way won't call the filters set on the controller.
I was able to do it with laravel 4.0 with the callAction method, passing the app and its router, but the method has changed now and the filters are called in the ControllerDispatcher class instead of the Controller class.
If you have routes declared for your classes then you may use something like this:
$request = Request::create('car/update', 'POST', array('id' => 10));
return Route::dispatch($request)->getContent();
In this case you have to declare this in routes.php file:
Route::post('car/update/{id}', 'CarController#update');
If you Use this approach then filters will be executed automatically.
Also you may call any filter like this (not tested but should work IMO):
$response = Route::callRouteFilter('filtername', 'filter parameter array', Route::current(), Request::instance());
If your filter returns any response then $response will contain that, here filter parameter array is the parameter for the filter (if there is any used) for example:
Route::filter('aFilter', function($route, $request, $param){
// ...
});
If you have a route like this:
Route::get('someurl', array('before' => 'aFilter:a_parameter', 'uses' => 'someClass'));
Then the a_parameter will be available in the $param variable in your aFilter filter's action.
So I might have found a solution to my problem, it might not be the best solution but it works. Don't hesitate to propose a better solution!
Route::patch('{class}/{id}', array(
'as' => 'object.update',
function ($class, $id) {
$router = app()['router']; // get router
$route = $router->current(); // get current route
$request = Request::instance(); // get http request
$controller = camel_case($class) . 'Controller'; // generate controller name
$action = 'update'; // action is update
$dispatcher = $router->getControllerDispatcher(); // get the dispatcher
// now we can call the dispatch method from the dispatcher which returns the
// controller action's response executing the filters
return $dispatcher->dispatch($route, $request, $controller, $action);
}
));
I'm trying to create a custom validation rule within laravel 4 and am struggling to get it to work or understand what I'm doing.
Currently - when I try to submit a form and validate it i get the following in my browser:
/**/
Which I'm taking as something is broken!
I have created a class in app/validators/customValidate.php
class CustomValidate extends Illuminate\Validation\Validator
{
public function uniqueMailchimp($attribute, $value, $parameters)
{
$mailchimp = MailchimpWrapper::lists()
->memberInfo(
'xxxxxxx',
array('emails'=>array(
'email'=>$value
)
));
//dd($$mailchimp['success_count']);
return ($mailchimp['success_count'] > 0 ? false : true );
}
I have run composer dump-autload
In my controller I am doing the following:
$rules = array(
'email' =>'email|uniqueMailchimp',
);
$messages = array(
'uniqueMailchimp'=>'The email provided has already been used.'
);
$validator = Validator::make($data, $rules, $messages);
}
I am then checking for a valid form with:
if($validator->passes()) {
# code
}
if validation fails the controller should redirect to the view:
return Redirect::route('members.create')
->withInput()
->withErrors($validator)
->with('errMessage', $message);
I've probably missed a step. I've seen posts about registering the rule in global.php but I'm not sure what I'm doing.
Any help appreciated
You are quite right - you need to register your rule with the validator. In any bootstrap-like file (a service provider is the best candidate if you have one, else app/start/global.php is good, and routes.php wouldn't be crazy either) you need the following code:
Validator::extend('foo', 'CustomValidate#uniqueMailchimp');
However, if you take a look at the docs you'll see that you don't need a whole class for this - you can just do it as a closure. A class is useful if you want the automatic IoC DI carried out for you though (although you don't appear to use that in your validator).
I'm trying to register a user in my application while keeping all business logic in the model and as little as possible in the controller. To accomplish this, I'm running user validation in the model's boot() method when the Class::creating() event fires. If the validation fails, I simply return false, cancelling the event. You can see this here:
public static function boot() {
parent::boot();
User::creating(function(){
$validator = new Services\Validators\RUser;
if (! $validator->passes()) return false;
});
}
The validator class you see is simply rules and it contains a getErrors() function.
My question is, how can I rewrite this so that I can retrieve the validator's errors for use in a conditional redirect later?
My controller postRegister() (the function called when clicking submit on form) looks like this:
public function postRegister() {
$user = new User(Input::all());
$user->save();
}
I know I'm not handling that in the controller correctly, so I would appreciate some advice with that as well.
Thanks.
You would set a 'protected $errors;' property on the User model, and then
User::creating(function(){
$validator = new Services\Validators\RUser;
if (! $validator->passes()) {
$this->errors = $validation->getErrors();
return false;
}
});
Not a direct answer to your question, but you should check out the Ardent package which is great for automatic model validation and has some other nice accompanying features. Internally it uses native Laravel validators so it's easy to use and will do just what you ask about. It really saves a lot of work (DRY) and I find it very useful.
https://github.com/laravelbook/ardent
The basics from the docs:
Ardent models use Laravel's built-in Validator class. Defining
validation rules for a model is simple and is typically done in your
model class as a static variable:
class User extends \LaravelBook\Ardent\Ardent {
public static $rules = array(
'name' => 'required|between:4,16',
'email' => 'required|email',
'password' => 'required|alpha_num|between:4,8|confirmed',
'password_confirmation' => 'required|alpha_num|between:4,8',
);
}
Ardent models validate themselves automatically when Ardent->save() is
called. You can also validate a model at any time using the
Ardent->validate() method.
$user = new User;
$user->name = 'John doe';
$user->email = 'john#doe.com';
$user->password = 'test';
$success = $user->save(); // returns false if model is invalid
When an Ardent model fails to validate, a
Illuminate\Support\MessageBag object is attached to the Ardent object
which contains validation failure messages.
Retrieve the validation errors message collection instance with
Ardent->errors() method or Ardent->validationErrors property.
Retrieve all validation errors with Ardent->errors()->all(). Retrieve
errors for a specific attribute using
Ardent->validationErrors->get('attribute').
So in the end you can do:
$user = new User;
$user->name = 'John doe';
$user->email = 'john#doe.com';
$user->password = 'test';
if(!$user->save())
{
print_r($user->errors()->all()); //or whatever else you wish to do on failure
}
I installed Laravel for the first time less than 12 hours ago, so i may be acting a little prematurely, but to my knowledge...
You have two main options, return the validator class, or store the errors in the User model. I'm currently working with the former, so i have a validate() method which returns the Validator class, which i then do the if($v->passes()) in my controller and can output errors in the controller via $v->messages().
Using your method, you will want to store your errors in the User object if you want to continue returning false on failure. So you could change:
if (! $validator->passes()) return false;
to
if (! $validator->passes()) {
$this->errors = $validator->messages();
return false;
}
and in your controller do:
if(isset($user->errors)) {
//loop and print errors from validator
}
N.B: just to reiterate, im a complete newbie to laravel so i may have gotten something completely wrong. But if i have, someone will correct me and we'll both have learned something :)
I seem to have come across an issue with Symfony 2 that I have not seen before (or more likley im missing something really obvious).
I had a route which took no parameters and worked perfectly, linking up to the controller and displaying the correct view. I then updated the route to take a single parameter like so:
# Note routing
gibbo_dummy_notes_Add:
pattern: /notes/add/{notebook}
defaults: { _controller: GibboDummyBundle:Note:add, notebook: 0}
requirements:
_method: POST|GET
However if I now try and access this route notes/add/test I get the error The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?. If I remove the parameter it works perfectly notes/add.
This setup is exactly the same as the rest of the routes that use parameters in my app.
The action in the controller definitely returns a response object. If I place a die('test'); at the top of the action and remove the parameter from the URL I reach the die statement and see 'test', but if I add the parameter to the URL it shows the error, so its clearly not reaching my controller or action when including the parameter but as far as I can tell the route is setup correctly, and im not getting the normal route not defined error I would expect to see if there was an issue with the url.
Im running it under the dev environment and I have tried other browsers / cleared cache etc. The action inside the controller NoteController looks like
public function addAction($notebook)
{
$pageTitle = 'Note';
$form = $this->createForm(new NoteType(), null, array(
'validation_groups' => array('add')
));
$request = $this->getRequest();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
/** #var $em \Doctrine\ORM\EntityManager */
$em = $this->getDoctrine()->getManager();
/** #var $note Note */
$note = $form->getData();
$note->setNotebook();
$em->persist($note);
$em->flush();
return $this->redirect($this->generateUrl('gibbo_dummy_note_View'));
}
}
return $this->render('GibboDummyBundle:Note:new.html.twig', array(
'pageTitle' => $pageTitle,
'form_add' => $form->createView(),
'selected' => 'codebooks'
));
}
Can anyone shed some light about what I might be doing wrong?
Please add the controller code for gibbo_dummy_note_View route.
Most likely, your redirection ends up in a bad controller method - the one that does not have return statement