PHPUnit how to write a Laravel Nova Observer test - php

I would like to write a test for my CommentObserver. This observer is only registered in the NovaServiceProvider but not the AppServiceProvider. This means I cannot test my observer by using my own Controllers.
In my eyes I have 3 ways to test my observer:
Either performing a feature test by sending a post request to the Nova API
Mocking the observer by calling the function in the observer to check if the function perfoms as desired
Trying to register my observer on the fly in the AppServiceProvider, performing a request and deregistering the observer in the AppServiceProvider again.
I tried to find a solution for any of these 3 ways to test my observer but unfortunately I faild with any of them.
Problems:
For way 1 I always get a validation error and Nova tells me that my input is invalid.
For way 2 I fail at mocking the observer function
For way 3 I didn't find any solution on how to register and deregister the oberserver on the fly at the AppServiceProvider
Do you guys have idea and solition on how I can test my CommentObserver (which is as written above only registered in my NovaServiceProvider).
Update:
So, here is the code of my observer. I need to have an valid request to test my observer in order to have the ability to access the $request->input('images') variable. I do know I can also use $comment->content instead of request()->input('content') because $comment->content already contains the new content which is not saved it this point.
The reason why I need a valid request is that the variable images is not part of the Comment model. So I cannot use $comment->images because it simply doesn't exist. That's why I need to access the request input. What my observer is basically doing is to extract the base64 images from the content, saves them to the server and replaces them by an image link.
class CommentObserver
{
public function updating(Comment $comment)
{
if (!request()->input('content')) {
return;
}
if (request()->input('content') == $comment->getRawOriginal('content')) {
return;
}
$images = request()->input('images');
if(!is_array($images)) {
$images = json_decode(request()->input('images'));
}
checkExistingImagesAndDeleteWhenNotFound($comment, request()->input('content'), 'comments', 'medium');
$comment->content = addBase64ImagesToModelFromContent($comment, request()->input('content'), $images, 'comments', 'medium');
}
}
This is my test so far. I choose way 1 but as described already this always leads to an validation error by the nova controller and I cannot figure out what is the error/what is missing or wrong.
class CommentObserverTest extends TestCase
{
/** #test */
public function it_test()
{
$user = User::factory()->create([
'role_id' => Role::getIdByName('admin')
]);
$product = Product::factory()->create();
$comment = Comment::factory()->create(['user_id' => $user->id, 'content' => '<p>Das ist wirklich ein super Preis!</p>', 'commentable_type' => 'App\Models\Product', 'commentable_id' => $product->id]);
$data = [
'content' => '<p>Das ist wirklich ein HAMMER Preis!</p>',
'contentDraftId' => '278350e2-1b6b-4009-b4a5-05b92aedaae6',
'pageStatus' => PageStatus::getIdByStatus('publish'),
'pageStatus_trashed' => false,
'commentable' => $product->id,
'commentable_type' => 'App\Models\Product',
'commentable_trashed' => false,
'user' => $user->id,
'user_trashed' => false,
'_method' => 'PUT',
'_retrieved_at' => now()
];
$this->actingAs($user);
$response = $this->put('http://nova.mywebsiteproject.test/nova-api/comments/' . $comment->id, $data);
dd($response->decodeResponseJson());
$das = new CommentObserver();
}
}
Kind regards and thank you

Why depend on the boot method in your NovaServiceProvider? It is possible to call the observe() method on the fly in your test:
class ExampleTest extends TestCase
{
/** #test */
public function observe_test()
{
Model::observe(ModelObserver::class);
// If you need the request helper, you can add input like so:
request()->merge([
'content' => 'test'
]);
// Fire model event by updating model
$model->update([
'someField' => 'someValue',
]);
// Updating should be triggered in ModelObserver
}
}
It should be now be possible in your observer class:
public function updating(Model $model)
{
dd(request()->input('content')); // returns 'test'
}

Related

PHPUNIT - Testing a callback that is a new instance of class

I have a class that calls a WordPress global function 'add_menu_page' This function receives a number of arguments, one of which is a callback that in this case creates a new instance of a class 'CreateAdminMountPoint'.
In my PHPUnit test I have mocked the admin_menu_page function and its expected 'with' arguments.
My problem lies with the callback class, in my test I have set the assertion to:
array(new CreateAdminMountPoint(array('slug' => 'brands')), 'addMountPoint')
This is obviously bad practice as my test now relies on a separate class rather than testing in isolation.
How can I change my test so it does not depend on CreateAdminMountPoint?
The Class
class CreateAdminMenus {
public function addMenuPages($tables) {
foreach( $tables as $menu ) {
add_menu_page(
$menu['title'],
$menu['title'],
$menu['wp-menu']['capability'],
'pup/' . $menu['title'] . '/edit.php',
[new CreateAdminMountPoint($menu), 'addMountPoint'], // Trying to test this without being dependant on CreatAdminMountPoint
$menu['wp-menu']['icon']
);
}
}
}
Test Case
class CreateAdminMenusTest extends LSMTestCase {
protected function setup() {
$this->tables = array(
'brands' =>
array(
'title' => 'title',
'slug' => 'brands',
'wp-menu' =>
array(
'capability' => 'capability',
'icon' => 'icon',
),
),
);
}
public function testMenusAreCreated() {
$adminMenus = new CreateAdminMenus;
$mock = $this->mockGlobalFunction('add_menu_page');
$mock->expects($this->exactly(1))
->method('add_menu_page')
->with(
'title',
'title',
'capability',
'pup/title/edit.php',
array(new CreateAdminMountPoint(array('slug' => 'brands')), 'addMountPoint'),
'icon'
);
$adminMenus->addMenuPages($this->tables);
}
}
The way your code is written now it's impossible to test it in any other way then you do now. Which actually isn't as bad as you might think. If I would see a test like this in a pull request I would approve it.
That being said, I can imagine that you do want to test it more thoroughly. Your best solution would be to use a CreateAdminMountPointBuilder class. So instead of creating the new object inline. You use your new class to build it. This way you can mock that class in your test and assert that the mocked response from the builder is put into your function call.
You can now also reuse the builder if you want to use it anywhere else in your project.
Hope it makes sense, otherwise I could add some sample code. Let me know!

How to check for required fields in object?

i have example object with fields
name => John
surname => Dow
job => engineer
and output form with placeholders. some required, some not.
what is best practice for check if it requred and show error with null fields?
There are multiple ways actually you can do that inside of controller method or make use of Laravels Request Classes for me I prefer to use Request Classes
look below I will list the two examples
Validate inside the controller's method
public function test(Request $request){
if($request->filled('name){
/*filled will check that name is set on the current
request and not empty*/
//Do your logic here
}
}
Second way is by using the Validator Facade inside your controller
use Validator;
class TestController{
public function test(Request $request){
$validator = Validator::make($request->all(), [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
/*continue with your logic here if the request failed on
validator test Laravel will automatically redirect back
with errors*/
}
}
Third way my favorite one personally
you can generate a Request class using this command
php artisan make:request AddBookRequest
that will generate the request class under "app/Http/Requests/AddBookRequest" , inside of any generated request class you will find two methods authorize() and rules()
in the authorized method you have to return truthy or falsy value this will detect if the current user making the request has authorization to fire this request inside of the rules method you do pretty much as you did in the Validator in the second way check the example
public function authorize(){
return true;
}
public function rules(){
return [
'title' => 'required|string',
'author_id' => 'required|integer'
];
}
then simply in your controller you can use the generated request like this
use App\Http\Requests\AddBookRequest;
public function store(AddBookRequest $request){
/* do your logic here since we uses a request class if it fails
then redirect back with errors will be automatically returned*/
}
Hope this helps you can read more about validation at
https://laravel.com/docs/5.6/validation
I think "simple is the best", just through object and check if properties exists
Ref: property_exists
Example:
if (property_exists($object, 'name')) {
//...do something for exists property
} else {
//...else
}

PrestaShop 1.7 Add new resources and class

I created new resources with this code:
class WebserviceRequest extends WebserviceRequestCore {
public static function getResources(){
$resources = parent::getResources();
// if you do not have class for your table
$resources['test'] = array('description' => 'Manage My API', 'specific_management' => true);
$resources['categoryecommerce'] = array('description' => 'o jacie marcin', 'class' => 'CategoryEcommerce');
$mp_resource = Hook::exec('addMobikulResources', array('resources' => $resources), null, true, false);
if (is_array($mp_resource) && count($mp_resource)) {
foreach ($mp_resource as $new_resources) {
if (is_array($new_resources) && count($new_resources)) {
$resources = array_merge($resources, $new_resources);
}
}
}
ksort($resources);
return $resources;
}
}
And new class:
class CategoryEcommerceCore extends ObjectModelCore {
public $category_id;
public $category_core_id;
public static $definition = array(
'table' => "category_ecommerce",
'primary' => 'category_id',
'fields' => array(
'category_core_id' => array('type' => self::TYPE_INT),
)
);
protected $webserviceParameters = array();
}
Webservice is override properly. My class WebserviceRequest is copying to
/override/classes/webservice/WebserviceRequest
but class isn't copying to /override/classes/ when i installing my module.
How to add new resourcess with own logic ? I want to add categories within relation to my table.
Regards
Martin
As soon as there is literally nothing regarding the API except Webkul tutorial... I tried to implement the "Webkul's" tutorial, but also failed. However seems that it's better to use hooks instead of overrides. I used my "reverse engineering skills" to determine the way to create that API, so-o-o-o, BEHOLD! :D
Let's assume you have a custom PrestaShop 1.7 module. Your file is mymodule.php and here are several steps.
This is an install method wich allows you to register the hook within database (you can uninstall and reinstall the module for this method to be executed):
public function install() {
parent::install();
$this->registerHook('addWebserviceResources');
return true;
}
Add the hook listener:
public function hookAddWebserviceResources($resources) {
$added_resources['test'] = [
'description' => 'Test',
'specific_management' => true,
];
return $added_resources;
}
That specific_management option shows you are going to use WebsiteSpecificManagement file instead of database model file.
Create WebsiteSpecificManagement file, called WebsiteSpecificManagementTest (Test - is CamelCased name of your endpoint). You can take the skeleton for this file from /classes/webservice/WebserviceSpecificManagementSearch.php. Remove everything except:
setObjectOutput
setWsObject
getWsObject
getObjectOutput
setUrlSegment
getUrlSegment
getContent (should return $this->output; and nothing more)
manage - you should rewrite it to return/process the data you want.
Add
include_once(_PS_MODULE_DIR_.'YOURMODULENAME/classes/WebserviceSpecificManagementTest.php');
to your module file (haven't figured out how to include automatically).
Go to /Backoffice/index.php?controller=AdminWebservice and setup the new "Auth" key for your application, selecting the test endpoint from the permissions list. Remember the key.
Visit /api/test?ws_key=YOUR_KEY_GENERATED_ON_STEP_4 and see the XML response.
Add &output_format=JSON to your URL to see the response in JSON.
You have to use something like $this->output = json_encode(['blah' => 'world']) within manage method at WebsiteSpecificManagementTest.

Unit Test Laravel's FormRequest

I am trying to unit test various custom FormRequest inputs. I found solutions that:
Suggest using the $this->call(…) method and assert the response with the expected value (link to answer). This is overkill, because it creates a direct dependency on Routing and Controllers.
Taylor’s test, from the Laravel Framework found in tests/Foundation/FoundationFormRequestTest.php. There is a lot of mocking and overhead done there.
I am looking for a solution where I can unit test individual field inputs against the rules (independent of other fields in the same request).
Sample FormRequest:
public function rules()
{
return [
'first_name' => 'required|between:2,50|alpha',
'last_name' => 'required|between:2,50|alpha',
'email' => 'required|email|unique:users,email',
'username' => 'required|between:6,50|alpha_num|unique:users,username',
'password' => 'required|between:8,50|alpha_num|confirmed',
];
}
Desired Test:
public function testFirstNameField()
{
// assertFalse, required
// ...
// assertTrue, required
// ...
// assertFalse, between
// ...
}
public function testLastNameField()
{
// ...
}
How can I unit test (assert) each validation rule of every field in isolation and individually?
I found a good solution on Laracast and added some customization to the mix.
The Code
/**
* Test first_name validation rules
*
* #return void
*/
public function test_valid_first_name()
{
$this->assertTrue($this->validateField('first_name', 'jon'));
$this->assertTrue($this->validateField('first_name', 'jo'));
$this->assertFalse($this->validateField('first_name', 'j'));
$this->assertFalse($this->validateField('first_name', ''));
$this->assertFalse($this->validateField('first_name', '1'));
$this->assertFalse($this->validateField('first_name', 'jon1'));
}
/**
* Check a field and value against validation rule
*
* #param string $field
* #param mixed $value
* #return bool
*/
protected function validateField(string $field, $value): bool
{
return $this->validator->make(
[$field => $value],
[$field => $this->rules[$field]]
)->passes();
}
/**
* Set up operations
*
* #return void
*/
public function setUp(): void
{
parent::setUp();
$this->rules = (new UserStoreRequest())->rules();
$this->validator = $this->app['validator'];
}
Update
There is an e2e approach to the same problem. You can POST the data to be checked to the route in question and then see if the response contains session errors.
$response = $this->json('POST',
'/route_in_question',
['first_name' => 'S']
);
$response->assertSessionHasErrors(['first_name']);
I see this question has a lot of views and misconceptions, so I will add my grain of sand to help anyone who still has doubts.
First of all, remember to never test the framework, if you end up doing something similar to the other answers (building or binding a framework core's mock (disregard Facades), then you are doing something wrong related to testing).
So, if you want to test a controller, the always way to go is: Feature test it. NEVER unit test it, not only is cumbersome to unit test it (create a request with data, maybe special requirements) but also instantiate the controller (sometimes it is not new HomeController and done...).
They way to solve the author's problem is to feature test like this (remember, is an example, there are plenty of ways):
Let's say we have this rules:
public function rules()
{
return [
'name' => ['required', 'min:3'],
'username' => ['required', 'min:3', 'unique:users'],
];
}
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HomeControllerTest extends TestCase
{
use RefreshDatabase;
/*
* #dataProvider invalid_fields
*/
public function test_fields_rules($field, $value, $error)
{
// Create fake user already existing for 'unique' rule
User::factory()->create(['username' => 'known_username']);
$response = $this->post('/test', [$field => $value]);
$response->assertSessionHasErrors([$field => $error]);
}
public function invalid_fields()
{
return [
'Null name' => ['name', null, 'The name field is required.'],
'Empty name' => ['name', '', 'The name field is required.'],
'Short name' => ['name', 'ab', 'The name must be at least 3 characters.'],
'Null username' => ['username', null, 'The username field is required.'],
'Empty username' => ['username', '', 'The username field is required.'],
'Short username' => ['username', 'ab', 'The username must be at least 3 characters.'],
'Unique username' => ['username', 'known_username', 'The username has already been taken.'],
];
}
}
And that's it... that is the way of doing this sort of tests... No need to instantiate/mock and bind any framework (Illuminate namespace) class.
I am taking advantage of PHPUnit too, I am using data providers so I don't need to copy paste a test or create a protected/private method that a test will call to "setup" anything... I reuse the test, I just change the input (field, value and expected error).
If you need to test if a view is being displayed, just do $response->assertViewIs('whatever.your.view');, you can also pass a second attribute (but use assertViewHas) to test if the view has a variable in it (and a desired value). Again, no need to instantiate/mock any core class...
Have in consideration this is just a simple example, it can be done a little better (avoid copy pasting some errors messages).
One last important thing: If you unit test this type of things, then, if you change how this is done in the back, you will have to change your unit test (if you have mocked/instantiated core classes). For example, maybe you are now using a FormRequest, but later you switch to other validation method, like a Validator directly, or an API call to other service, so you are not even validating directly in your code. If you do a Feature Test, you will not have to change your unit test code, as it will still receive the same input and give the same output, but if it is a Unit Test, then you are going to change how it works... That is the NO-NO part I am saying about this...
Always look at test as:
Setup minimum stuff (context) for it to begin with:
What is your context to begin with so it has logic ?
Should a user with X username already exist ?
Should I have 3 models created ?
Etc.
Call/execute your desired code:
Send data to your URL (POST/PUT/PATCH/DELETE)
Access a URL (GET)
Execute your Artisan Command
If it is a Unit Test, instantiate your class, and call the desired method.
Assert the result:
Assert the database for changes if you expected them
Assert if the returned value matches what you expected/wanted
Assert if a file changed in any desired way (deletion, update, etc)
Assert whatever you expected to happen
So, you should see tests as a black box. Input -> Output, no need to replicate the middle of it... You could setup some fakes, but not fake everything or the core of it... You could mock it, but I hope you understood what I meant to say, at this point...
Friends, please, make the unit-test properly, after all, it is not only rules you are testing here, the validationData and withValidator functions may be there too.
This is how it should be done:
<?php
namespace Tests\Unit;
use App\Http\Requests\AddressesRequest;
use App\Models\Country;
use Faker\Factory as FakerFactory;
use Illuminate\Routing\Redirector;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
use function app;
use function str_random;
class AddressesRequestTest extends TestCase
{
public function test_AddressesRequest_empty()
{
try {
//app(AddressesRequest::class);
$request = new AddressesRequest([]);
$request
->setContainer(app())
->setRedirector(app(Redirector::class))
->validateResolved();
} catch (ValidationException $ex) {
}
//\Log::debug(print_r($ex->errors(), true));
$this->assertTrue(isset($ex));
$this->assertTrue(array_key_exists('the_address', $ex->errors()));
$this->assertTrue(array_key_exists('the_address.billing', $ex->errors()));
}
public function test_AddressesRequest_success_billing_only()
{
$faker = FakerFactory::create();
$param = [
'the_address' => [
'billing' => [
'zip' => $faker->postcode,
'phone' => $faker->phoneNumber,
'country_id' => $faker->numberBetween(1, Country::count()),
'state' => $faker->state,
'state_code' => str_random(2),
'city' => $faker->city,
'address' => $faker->buildingNumber . ' ' . $faker->streetName,
'suite' => $faker->secondaryAddress,
]
]
];
try {
//app(AddressesRequest::class);
$request = new AddressesRequest($param);
$request
->setContainer(app())
->setRedirector(app(Redirector::class))
->validateResolved();
} catch (ValidationException $ex) {
}
$this->assertFalse(isset($ex));
}
}

Possible to create a factory to instantiate custom Form validators?

(using Zend Framework 2.2.4)
My validator factory, doesn't seem to "exist" at validation time. If I attempt to instantiate the validator from the controller in which the form is housed, it conversely works fine:
This works...
$mycustomvalidator = $this->getServiceLocator()
->get('ValidatorManager')
->get('LDP_PinAvailable');
Here's how things are set up otherwise in the code, I can't seem to find the problem, and was hopeful to avoid opening up ZF2 source to understand. By way of documentation, it seems right.
Module Config
public function getValidatorConfig()
{
return array(
'abstract_factories' => array(
'\LDP\Form\Validator\ValidatorAbstractFactory',
),
);
}
Factory Class
namespace LDP\Form\Validator;
use Zend\ServiceManager\AbstractFactoryInterface,
Zend\ServiceManager\ServiceLocatorInterface;
class ValidatorAbstractFactory implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
return stristr($requestedName, 'LDP_PinAvailable') !== false;
}
public function createServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
// baked in for sake of conversation
$validator = new \LDP\Form\Validator\PinAvailable();
if( $validator instanceof DatabaseFormValidatorInterface )
$validator->setDatabase( $locator->get('mysql_slave') );
return $validator;
}
}
Custom Validator
namespace LDP\Form\Validator;
class PinAvailable extends \Zend\Validator\AbstractValidator implements DatabaseFormValidatorInterface
{
/**
* #var \Zend\Db\Sql\Sql
*/
private $database;
public function setDatabase( \Zend\Db\Sql\Sql $db )
{
$this->database = $db;
}
public function isValid( $value )
{
$DBA = $this->database->getAdapter();
// do the mixed database stuff here
return true;
}
}
Lastly, the form field validator config part of the array:
'pin' => array(
'required' => true,
'filters' => array(
array('name' => 'alnum'),
array('name' => 'stringtrim'),
),
'validators' => array(
array( 'name' => 'LDP_PinAvailable' )
),
),
),
Piecing it all together, the form loads, and when submitted, it does with the stack trace below:
2013-10-28T17:09:35-04:00 ERR (3): Exception:
1: Zend\Validator\ValidatorPluginManager::get was unable to fetch or create an instance for LDP_PinAvailable
Trace:
#0 /Users/Saeven/Documents/workspace/Application/vendor/zendframework/zendframework/library/Zend/ServiceManager/AbstractPluginManager.php(103): Zend\ServiceManager\ServiceManager->get('LDP_PinAvailabl...', true)
#1 /Users/Saeven/Documents/workspace/Application/vendor/zendframework/zendframework/library/Zend/Validator/ValidatorChain.php(82): Zend\ServiceManager\AbstractPluginManager->get('LDP_PinAvailabl...', Array)
The ValidatorPluginManager extends the Zend\ServiceManager\AbstractPluginManager. The AbstractPluginManager has a feature called "autoAddInvokableClass", which is enabled by default.
Basically, what this means, is that if the service name requested can't be resolved by the ValidatorPluginManager, it will then check if the name is a valid class name. If so, it will simply add it as an invokable class right there, on-demand, which of course means that it will never fall back to your abstract factory.
To circumvent this behavior, the easiest method is to simply make your abstract factory respond to service names that do not actually resolve to the actual class names.
See: AbstractPluginManager.php#L98-L100
Digging some more, I've found the problem. It distilled to these lines in Zend\Validator\ValidatorChain circa line 80:
public function plugin($name, array $options = null)
{
$plugins = $this->getPluginManager();
return $plugins->get($name, $options);
}
There was no plugin manager available in context.
It took about three seconds of Googling to find that I had to do this when I prepared the form in the controller:
$validators = $this->getServiceLocator()->get('ValidatorManager');
$chain = new ValidatorChain();
$chain->setPluginManager( $validators );
$form->getFormFactory()->getInputFilterFactory()->setDefaultValidatorChain( $chain );
Hopefully this helps someone else. You are able to use regular old classnames when setting it up this way, no need to warp the classnames.
In ZF3/Laminas, if a validator is registered as an invokable, you can call the validator in the getInputFilterSpecification() of your form, and no problem. If a validator is instantiated using a factory, you get into trouble. If I understand correctly, even if your form is registered like this
'form_elements' => [
'factories' => [
SomeForm::class => SomeFormFactory::class,
]
]
and your validator:
'validators' => [
'factories' => [
SomeValidator::class => SomeValidatorFactory::class,
]
]
you won't be instantiating the validator via factory. The reason is that the form factory (the one you get like $form->getFormFactory()) has an input filter factory and in there sits default validator chain. And this validator chain has no ValidatorManager attached. And without the ValidatorManager, the default chain cannot map the validator name to the validator factory.
To solve all this headache, in your controller factory do this:
$form->('FormElementManager')->get(SomeForm::class);
$form->getFormFactory()->getInputFilterFactory()
->getDefaultValidatorChain()->setPluginManager($container->get('ValidatorManager'));
and your troubles are over.

Categories