Laravel Testing - Should I be creating dependant resources in each test? - laravel

Starting on a new build with Laravel Spark 6 (Laravel 5.6) and decided to give TDD a try.
First test was lovely, I created a unit test to make sure that users can create an team.
(Pseudo code):
class AddNewTeamTest extends TestCase
{
/** #test */
public function admin_can_create_new_team()
{
// Create a user account
$data = [
// Information for tea,
];
$response = $this->withHeaders([
'X-Requested-With' => 'XMLHttpRequest',
])
->actingAs($user)
->json('POST', '/api/teams', $data);
$response
->assertStatus(201);
}
}
Using this in TDD style was a nice process, but now I want to be able to write a test for adding a member to that team.
It seems backwards that in this new test, I would run all of the code in my first test. Is there anyway around this? For the new test I would need a user and team already created before I could test adding a user to that team..
Any links or advice welcome!

You can use function setUp() and build your enviroment inside it.
So your class should looks like that:
class AddNewTeamTest extends TestCase
{
protected function setUp()
{
// Create a user account
// Create your enviroment, etc.
$this->actingAs($user)
}
/** #test */
public function admin_can_create_new_team()
{
$data = [
// Information for tea,
];
$response = $this->withHeaders([
'X-Requested-With' => 'XMLHttpRequest',
])
->json('POST', '/api/teams', $data);
$response
->assertStatus(201);
}
public function testAnother()
{
\\your next test
}
}
If you need a team in next few cases, that should be added in setUp().
Also, you can make your next test needed ypur previous one. In that case you can return something in admin_can_create_new_team() and take as parameter in testAnother()
More info:
https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.test-dependencies

Related

Laravel request remove fields before validation

I am trying to remove some fields before they are validated.
Trying to attempt this inside prepareForValidation()
use Illuminate\Foundation\Http\FormRequest;
class VideoRequest extends ApiRequest
{
// ..code..
protected function prepareForValidation()
{
$this->merge([
'public' => $this->toBoolean($this->public),
'notify' => $this->toBoolean($this->notify),
]);
$video_id = $this->route('video_id');
if($this->isMethod('put') && Video::salesStarted($video_id)){
Log::info('removing sales');
// attempt 1
$this->except(['sales_start', 'tickets', 'price']);
// attempt 2
$this->request->remove('sales_start');
// attempt 3
$this->offsetUnset('sales_start');
}
if($this->isMethod('put') && Video::streamStarted($video_id)){
Log::info('removing streams');
// attempt 1
$this->except(['stream_start', 'stream_count', 'archive']);
// attempt 2
$this->request->remove('sales_start');
// attempt 3
$this->offsetUnset('sales_start');
}
$thumb = $this->uploadThumbnail($video_id);
if($thumb !== null){
$this->merge($thumb);
}
}
// ..code..
}
I made sure the code was entering inside the if statement, however the fields are not being removed.
Running $this->request->remove() and $this->except() have no effect.
If I add safe() it throws Call to a member function safe() on null.
I also tried to use unset() but nothing seems to work.
The rules for the dates are like so:
'sales_start' => 'sometimes|required|date|after:now|before:stream_start',
'stream_start' => 'sometimes|required|date|after:now',
but the $request->validated() returns the errors although it shouldn't be validating the deleted fields.
"sales_start": [
"The sales start must be a date after now."
],
"stream_start": [
"The stream start must be a date after now."
]
Why are the fields not being deleted?
Edit
As requested I added some code.
This is what ApiRequest looks like:
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
abstract class ApiRequest extends FormRequest
{
protected function failedValidation(Validator $validator): void
{
$response['data'] = [];
$response['api_status'] = 'ng';
$response['status_message'] = 'failed validation';
$response['errors'] = $validator->errors()->toArray();
throw new HttpResponseException(
response()->json( $response, 422 )
);
}
protected function toBoolean($booleable)
{
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}
And the request is called from the controller like so:
public function update(VideoRequest $request, $video_id)
{
... some code ...
$validated = $request->validated();
... some code ...
}
so $this refers to the VideoRequest that extends FormRequest.
Can't find anything about deleting. But acording to Laravel docs you pick what keys you need from a request as follows:
$request->only(['username', 'password']);
// plug everything you need into the array.
$request->except(['username', 'password']);
//plug everything you don't need into the array
The latter is probably most useful to you.
Example:
Say I have the following keys: ['username', 'password', 'randomVal'];
$request->only(['username', 'password']);
// Output:
['username', 'password']
$request->except(['username', 'password']);
// Output:
['randomVal']
To remove (unset) a key from a Request before it goes to the Controller you can use offsetUnset()
inside your request:
protected function prepareForValidation()
{
$this->offsetUnset('sales_start');//same goes for the other key to remove...
}
This is a bit of an ugly answer.
Instead of modifying the request before the validation, I tried adding exclude when getting rules().
So something along these lines:
public function rules() {
$ex = $this->isMethod('put') && Video::salesStarted($video_id) ? 'exclude|' : '';
return [
'sales_start' => $ex.'sometimes|required|other_stuff',
];
}
Note that the validation 'exclude' only works if added first.
So this won't work:
'sometimes|required|other_stuff|exclude' //exclude is called last
I am still unable to find out why remove(), exclude(), offsetUnset() were not working, so this is not the right answer, but I hope it helps if someone is having the same issue.
Edit
Setting this as correct answer as I was unable to find an alternative solution/fix.

Unit Testing with Model::Find Fails in Laravel

I am creating unit tests for my rather large Laravel application.
This test passes all assertions. It creates a new database entry for the advertising status model.
/** #test **/
public function create_a_new_advertising_status_test()
{
$status = AdvertisingStatus::create(['name' => 'Test']);
$this->assertNotNull($status);
$this->assertCount(1, AdvertisingStatus::all());
$this->assertEquals($status->name, $this->advertisingStatus['name']);
}
This test fails the NotNull assertion as it says $status is null, which means that the Find method is failing.
/** #test **/
public function edit_an_existing_advertising_status_entry()
{
$status = AdvertisingStatus::create(['name' => 'Test']);
// $status = AdvertisingStatus::first(); // This successfully finds the database entry
$status = AdvertisingStatus::find(1); // This fails to find the database entry
$status->name = 'Edit';
$status->save();
$this->assertNotNull($status);
$this->assertCount(1, AdvertisingStatus::all());
$this->assertEquals($status->name, "Edit");
}
It appears like the find function and subsequently the where function takes too long to locate the entry, so the test renders the $status variable null.
Aside from using the Model::first() function, does anyone have any idea how to overcome this?
I'm wondering if it's because my application is extremely large and takes a long time to run because I'm using RefreshDatabase
You have to create a factory first, then you will use it like this:
$status = factory(AdvertisingStatus::class)->create(['name' => 'Test']);

Map controller functions into database records in Laravel

I try to implement a chain of approval methodology in my Laravel app.
Lets say that I have a standard CRUD controller with standard REST routes.
[URL]/products
In the controller I have 3 functions (Index, Store, Update)
I want that for Store and Update a certain condition will happen before, something like that (pseudo):
if (fucntion requires chain of approval) {
if (auth()->user !== one of the approvers){
email all approvers;
return 201 "pending approval";
}
}
// the logged in user is allowed to execute the function
rest of the code...
I'm struggling with a few things here:
The only thing I can think of something that might be similar to this inside Laravel is the $this->authorize() function but I don't think that it was meant to be used like this, it is meant to authorize or not to authorize, not for 201 codes, and using it means aborting with 201 and it doesn't sit right
I want to allow the admins to control which functions need approval and because I don't want to maintain my controller functions together with a seeder that will contain a list of all the functions I'm thinking about creating an artisan command to be run before production and mapping all the functions into a database table that will be used as a model and all the process will use a proper many to many relations between the functions and the approvers, but I don't know how to map the functions with artisan command and I don't know if this is even the right way to go
I want to avoid from writing a certain code in all the functions that might require approval, and don't know how and if it is even possible
The return of the functions should return a JsonResource of the specific model for example ProductResource, What should I return when I need approval? Just to mock a proper response with status pending?
Thanks for everybody who is willing to give me some guidance.
You can do something similar to below... just have a single class that defines how each user type (or even permission) is handled.
public function index(IndexRequest $request) // verify that use is authorized to do this action
{
$response = (new MyJobDirector)->handle(Auth::user());
.. handle response
}
MyJobDirector Class
class MyJobDirector
{
const STRATEGY = [
'user' => 'userHandler',
'manager' => 'managerHandler',
];
public function handle(User $user): array
{
return $this->{static::STRATEGY[$user->role]}();
}
protected function userHandler(): array
{
event(EmailApprovers::class);
return [
'code' => 201,
'response' => 'pending approval',
];
}
}
You can make it even more complicated by defining a separate class for each handler and specifying constants for status and a response that each class should return
class User extends BaseJobHandle
{
const CODE = 201;
const RESPONSE = 'pending approval';
}
abstract class BaseJobHandler
{
const CODE;
const RESPONSE;
public function handle(): array
{
$this->additionalProcesses();
return [
'code' => static::CODE,
'response' => static::RESPONSE,
];
}
protected function additionalProcesses(): void {}
}
class MyJobDirector
{
const STRATEGY = [
'user' => User::class,
'manager' => Manager::class,
];
public function handle(User $user): array
{
$class = static::STRATEGY[$user->role];
return (new $class)->handle();
}
}
Regarding The return of the functions should return a JsonResource of the specific model for example ProductResource, What should I return when I need approval? Just to mock a proper response with status pending?
You can have a ProductResouce class, in which you decide which specific resource to return depending on a use case. It's all depends on what data you need to return.

Authenticate user without using factory in unit testing

I am new to unit testing. I want to authenticate a user without using Factory. I want my testing code to be simple. I don't know how to use the Factory. Here is my code :
public function loginVerify()
{
$user = factory('App\User')->create();
}
the first thing that you have to do is follow the naming convention
Change
public function loginVerify()
{
$user = factory('App\User')->create();
}
to
public function testLoginVerify()
{
$user = factory('App\User')->create();
}
always use the test as a prefix for your testing function name.
and now as we look at your question, you can simply do this...
public function testLoginVerify()
{
$user_details = [
'email' => 'demo#gmail.com', // the email of a particular user
'password' => 'password' // the password of that user
];
$this->post('/login', $user_details)->assertRedirect('/home');
}
This is the very simplest way to do this.

Laravel Dusk screenshot

I'm using laravel 5.6 and Dusk for running some tests.
I'm always taking my screenshot like this
...
use Facebook\WebDriver\WebDriverDimension;
...
class LoginTest extends DuskTestCase
{
public function testLogin()
{
$user = User::first();
$this->browse(function ($browser) use ( $user ) {
$test = $browser->visit( new Login)
->resize(1920,1080)
...
->driver->takeScreenshot(base_path('tests/Browser/screenshots/testLogin.png'));
});
}
}
But as my tests will be more and more used, I don't want to continue to write everytime ->resize(X,Y) and base_path('bla/blab/bla').
I wanted to define the size and path for every tests that will be written.
I guess I should define some function in tests/DesukTestCase.php but I'm not even aware of how I can retrieve the driver and so on.
Have you got some guidance or documentation about this? Because I can't find anything...
In my DuskTestCase file I have the below in my driver() function.
protected function driver()
{
$options = (new ChromeOptions())->addArguments([
'--disable-gpu',
'--headless',
]);
$driver = RemoteWebDriver::create(
'http://selenium:4444/wd/hub',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
)
);
$size = new WebDriverDimension(1280, 2000);
$driver->manage()->window()->setSize($size);
return $driver;
}
You should just be able to configure it with the right dimensions you need.
You only need to add '--window-size=1920,1080' in $options. This will apply a 1920x1080 screen resolution to all your Dusk tests. Feel free to adjust to whatever window size you want.
So your DuskTestCase.php file should look like this:
protected function driver()
{
$options = (new ChromeOptions())->addArguments([
'--disable-gpu',
'--headless',
'--window-size=1920,1080',
]);
$driver = RemoteWebDriver::create(
'http://selenium:4444/wd/hub',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
)
);
}
Regarding the path issue, you can set it with Browser::$storeScreenshotsAt in setUp method of your test case class.
protected function setUp()
{
parent::setUp();
Browser::$storeScreenshotsAt = '/path/to/your/screenshots';
}
Default location of Browser::$storeScreenshotsAt is set in setUp method of the grand parent test case class.
So, make sure that you set Browser::$storeScreenshotsAt after calling parent::setUp(), otherwise it will be overwritten by the default.

Resources