I'm using Laravel for my project and I'm new to unit/feature testing so I was wondering what is the best way to approach more complicated feature cases when writing tests?
Let's take this test example:
// tests/Feature/UserConnectionsTest.php
public function testSucceedIfConnectAuthorised()
{
$connection = factory(Connection::class)->make([
'sender_id' => 1,
'receiver_id' => 2,
'accepted' => false,
'connection_id' => 5,
]);
$user = factory(User::class)->make([
'id' => 1,
]);
$response = $this->actingAs($user)->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id,
]
);
$response->assertLocation('/')->assertStatus(200);
}
So we've got this situation where we have some connection system between two users. There is a Connection entry in the DB created by one of the users. Now to make it a successful connection the second user has to approve it. The problem is within the UserController accepting this through connectionRequest:
// app/Http/Controllers/Frontend/UserController.php
public function connectionRequest(Request $request)
{
// we check if the user isn't trying to accept the connection
// that he initiated himself
$connection = $this->repository->GetConnectionById($request->get('request_id'));
$receiver_id = $connection->receiver_id;
$current_user_id = auth()->user()->id;
if ($receiver_id !== $current_user_id) {
abort(403);
}
[...]
}
// app/Http/Repositories/Frontend/UserRepository.php
public function GetConnectionById($id)
{
return Connection::where('id', $id)->first();
}
So we've got this fake (factory created) connection in the test function and then we unfortunately are using its fake id to run a check within the real DB among real connections, which is not what we want :(
Researching I found an idea of creating interfaces so then we can provide a different method bodies depending if we're testing or not. Like here for GetConnectionById() making it easy to fake answers to for the testing case. And that seems OK, but:
for one it looks like a kind of overhead, besides writing tests I have to make the "real" code more complicated itself for the sole purpose of testing.
and second thing, I read all that Laravel documentation has to say about testing, and there is no one place where they mention using of interfaces, so that makes me wonder too if that's the only way and the best way of solving this problem.
I will try to help you, when someone start with testing it is not easy at all, specially if you don't have a strong framework (or even a framework at all).
So, let me try help you:
It is really important to differentiate Unit testing vs Feature testing. You are correctly using Feature test, because you want to test business logic instead of a class directly.
When you test, my personal recommendation is always create a second DB to only use with tests. It must be completely empty all the time.
So, for you to achieve this, you have to define the correct environment variables in phpunit.xml, so you don't have to do magic for this to work when you only run tests.
Also, use RefreshDatabase trait. So, each time you run a test, it is going to delete everything, migrate your tables again and run the test.
You should always create what you need to have as mandatory for your test to run. For example, if you are testing if a user can cancel an order he/she created, you only need to have a product, a user and an invoice associated with the product and user. You do not need to have notifications created or anything not related to this. You must have what you expect to have in the real case scenario, but nothing extra, so you can truly test that it fully works with the minimum stuff.
You can run seeders if your setup is "big", so you should be using setup method.
Remember to NEVER mock core code, like request or controllers or anything similar. If you are mocking any of these, you are doing something wrong. (You will learn this with experience, once you truly know how to test).
When you write tests names, remember to never use if and must and similar wording, instead use when and should. For example, your test testSucceedIfConnectAuthorised should be named testShouldSucceedWhenConnectAuthorised.
This tip is super personal: do not use RepositoryPattern in Laravel, it is an anti-pattern. It is not the worst thing to use, but I recommend having a Service class (do not confuse with a Service Provider, the class I mean is a normal class, it is still called Service) to achieve what you want. But still, you can google about this and Laravel and you will see everyone discourages this pattern in Laravel.
One last tip, Connection::where('id', $id)->first() is exactly the same as Connection::find($id).
I forgot to add that, you should always hardcode your URLs (like you did in your test) because if you rely on route('url.name') and the name matches but the real URL is /api/asdasdasd, you will never test that the URL is the one you want. So congrats there ! A lot of people do not do this and that is wrong.
So, to help you in your case, I will assume you have a clear database (database without tables, RefreshDatabase trait will handle this for you).
I would have your first test as this:
public function testShouldSucceedWhenConnectAuthorised()
{
/**
* I have no idea how your relations are, but I hope
* you get the main idea with this. Just create what
* you should expect to have when you have this
* test case
*/
$connection = factory(Connection::class)->create([
'sender_id' => factory(Sender::class)->create()->id,
'receiver_id' => factory(Reciever::class)->create()->id,
'accepted' => false,
'connection_id' => factory(Connection::class)->create()->id,
]);
$response = $this->actingAs(factory(User::class)->create())
->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id
]
);
$response->assertLocation('/')
->assertOk();
}
Then, you should not change anything except phpunit.xml environment variables pointing to your testing DB (locally) and it should work without you changing anything in your code.
Related
So I have these variables $skipValidation (set specifically to FALSE), $validationRules, $validationMessages set according to this documentation
but for the life of me I can't figure out what trigger this $validationRules to run, I just assume that $skipValidation meant Codeigniter-4 already got me covered (automatically validates input before doing any queries)..
I even put $UserModel->errors() in case the validation rules catch an error
if($userModel->insert($data) === false) {
return view('form', ['validation' => $userModel->errors()])
} else {
redirect()->to('home');
}
I have these rules required and min_length[] applied to $validationRules but the model just skips the validationRules and insert it immediately to database rendering $validationRules useless..
Any ideas how to get validationRules in Models working? or how is it supposed to be used? I keep looping in the documentation because I don't know any better.
Dumb me, i was trying to figure out what is wrong with the code this whole time when its just a simple problem.. i have two models with the same filename (backup project) and i blindly edits model file that is inside the backup project..
Perhaps for future readers seeking an answer, don't forget to check your folder path for your model file, you might make the same mistake as i did.
Been struggling with how to do this the most optimized way possible...
I have two models: Catalog and Application.
Catalog has a field called name.
Application has a field called name.
Both have a relationship with each other.
I am struggling to find a way to create a function i could use across my Laravel application which i would pass application.id to it and it would return a $app->name value based on the following logic:
if $application->name exists, use this value as the $app->name for the $application object
otherwise, get the $catalog->name value and use it as the $app->name
Note that I would like to create a component #application() where i can simply pass the $application->id and build the display logic (theming/styling) into it.
Since i display this $app->name in many places, i would like to make it as lightweight as possible to avoid unnecessary queries.
I hope this makes sense! There are probably so many ways to go with it, i am lost at figuring out the way way to do this :(
I'm not completely sure to understand your model/DB design, but you could use a custom Helper to use that function through the whole app.
For that, you can create a simple PHP class Helper.php file in app/Http/Helpers folder or whatever location you want. Something like:
<?php
use App\Catalog;
use App\Application;
if (! function_exists('getAppName')) {
function getAppName($id){
// Do your logic here to return the name
$catalog = Catalog::find($id);
return $catalog->name;
}
}
?>
Then in any controller or view, you just do
getAppName($application->id)
Do no forget to add your helpers file to the composer autoload. So in composer.json in Laravel's root folder, add the helper path to the autoload array:
"files": [
"app/Http/Helpers/helpers.php"
],
Last but not least, run the following command:
composer dump-autoload
Please note that function logic is just for sample purposes since I don't know your model structure.
In my opinion, I care about the database cost.
Use ternary expression will be elegant. But it took two times IO costs from database if application name is empty.
$app_name = Application::find($id)->name;
$app_name = empty($app_name) ? Catalog::where('application_id', $id)->first()->name;
And this will more complicated, but the catalog_query only execute when application.name is empty, it execute in database and the result is taken out only once;
And Database will only find the name from one table or two table.
Something like this:
$catalog_query = Catalog::where('catalogs.application_id', $id)->select('catalogs.name')->groupBy('catalogs.name');
// if catalogs and applications relationship is 1:1, use ->limit(1) or remove groupBy('name') is better.
Application::where("applications.id", $id)
->selectRaw("IF(application.name IS NULL OR application.name = '', (" . $catalog_query->toSql() ."), applications.name ) AS app_name")
->mergeBindings($catalog_query->getQuery())
->first()
->app_name;
Hope this will help you.
After some searching, I succesfully installed the Authority-l4 package to use for my Laravel project. The docs are clear but small (not much info/examples). This is what my config file looks like atm:
return array[
'initialize' => function($authority) {
$user = $authority->getCurrentUser();
$authority->addAlias('manage', ['create', 'read', 'update', 'delete']);
if($user->hasRole('admin')) {
//Admin can manage all resources
$authority->allow('manage', 'all');
}
// User can manage his own post
Authority::allow('manage', 'User', function($self, $user){
return $self->getCurrentUser()->id === $user->id;
});
// User can manage his own post
Authority::allow('manage', 'Post', function($self, $post){
return $self->getCurrentUser()->id === $post->id;
});
}
];
I have some questions about this:
How to add a role to a user? hasRole() exists, why not setRole()?
I noticed nothing gets saved into the database, isn't this better?
How do I use my database with Authority? Could someone give me a head start, I've been strugling four hours now.
In some articles they say that the class Role should be changed to have many permissions instead of a user having many permissions, isn't this better?
Probably I'm thinking way to difficult about this package, searching the internet doesn't help either. Any help is appreciated!
I'm the author of Authority, and I maintain Authority-l4 though it was written primarily by Conar Welsh.
Since roles, as defined in the package, are just an Eloquent relation so you can simply add them like any other relation in Eloquent.
I have no idea what you're asking here, can you rephrase?
Can you elaborate on the question beyond what's in the readme (the part just above General Usage)?
Probably - either works. You don't need to use this structure to use Authority-l4. It's just an optional structure that you are 100% free to setup as you'd like. I personally don't use this at all and just use the Authority facade that it generates. Most of my permissions aren't stored in the DB though so that plays a factor.
The idea behind Authority is that it is implementation agnostic. It genuinely does not care where you store your data, you just need to tell authority what to do with your rules. Reading the section of the readme referenced above and the readme on the Authority core repo should be able to give you a general idea of how it expects information to be loaded - anything beyond that is up to your discretion.
i have create module using module builder , now i am having a field called as book Name
now if i give same book name 2 time t is accepting .
i don't want to use and plug in for checking duplicate value because i want to learn the customization through code .
so i can call ajax and check in data base weather the same book name is exist in db or not but i don't know how controller works in sugar crm . and how to call ajax in sugar crm .
can any one guide me , your help is much appreciated .
If you really want to accomplish this using ajax then I'd recommend an entryPoint as the way to go. This customization will require a couple of simple things. First you'll write a little bit of javascript to perform the actual ajax call. That ajax call will post to the entryPoint you write. The entryPoint will run the query for you and return a response to you in the edit view. So lets get started by writing the entryPoint first.
First, open the file custom/include/MVC/Controller/entry_point_registry.php. If the folder structure and file do not exist yet, go ahead and create them.
Add the following code to the entry_point_registry.php file:
$entry_point_registry['test'] = array('file' => 'custom/test.php', 'auth' => true);
Some quick explanation about that line:
The index value of test can be changed to whatever you like. Perhaps 'unique_book_value' makes more sense in your case. You'll see how this value is used in a minute.
The file value in the array points to where you're gonna put your actual code. You should also give this a more meaningful name. It does NOT need to match the array key mentioned above.
The 'auth' => true part determines whether or not the browser needs to have an active logged in session with SugarCRM or not. In this case (and almost all) I'd suggest keeping this to true.
Now lets look at the code that will go in custom/test.php (or in your case unique_book_name.php):
/* disclaimer: we are not gonna get all crazy with using PDO and parameterized queries at this point,
but be aware that there is potential for sql injection here. The auth => true will help
mitigate that somewhat, but you're never supposed to trust any input, blah blah blah. */
global $db; // load the global sugarcrm database object for your query
$book_name = urldecode($_REQUEST['book_name']); // we are gonna start with $_REQUEST to make this easier to test, but consider changing to $_POST when confirmed working as expected
$book_id = urldecode($_REQUEST['book_id']); // need to make sure this still works as expected when editing an existing record
// the $db->quote is an alias for mysql_real_escape_string() It still does not protect you completely from sql injection, but is better than not using it...
$sql = "SELECT id FROM book_module_table_name WHERE deleted = 0 AND name = '".$db->quote($book_name)."' AND id <> '".$db->quote($book_id)."'";
$res = $db->query($sql);
if ($db->getRowCount($res) > 0) {
echo 'exists';
}
else {
echo 'unique';
}
A note about using direct database queries: There are api methods you can use to accomplish this. (hint: $bean->retrieve_by_string_fields() - check out this article if you wanna go that route: http://developer.sugarcrm.com/2012/03/23/howto-using-the-bean-instead-of-sql-all-the-time/) However, I find the api to be rather slow and ajax should be as fast as possible. If a client asked me to provide this functionality there's a 99% chance I'd use a direct db query. Might use PDO and parameterized query if I'm feeling fancy that day, but it's your call.
Using the above code you should be able to navigate to https://crm.yourdomain.com/index.php?entryPoint=test and run the code we just wrote.
However at this point all you're gonna get is a white screen. If you modify the url to include the entryPoint part and it loads your home page or does NOT go to a white screen there are 3 potential causes:
You put something different for $entry_point_registry['test']. If so change the url to read index.php?entryPoint=whatever_you_put_as_the_array_key
You have sugar in a folder or something on your domain so instead of crm.yourdomain.com it is located somewhere ugly and stupid like yourdomain.com/sugarcrm/ if this is the case just make sure that your are modifying the url such that the actual domain portion is preserved. Okay I'll spell it out for you... https://yourdomain.com/sugarcrm/index.php?entryPoint=test
This is more rare, but for some reason that I cannot figure out apache sometimes needs to be reloaded when adding a new entrypoint. If you have shell access a quick /etc/init.d/apache2 reload should do the trick. If you don't have shell access you may need to open a ticket with your hosting provider (or get a fricking vps where you have some control!!!, c'mon man!)
Still not working? Did you notice the "s" in https? Try http instead and buy a fricking $9 ssl cert, geez man!
Okay moving on. Let's test out the entryPoint a bit. Add a record to the book module. Let's add the book "War of Art" (no, not Art of War, although you should give that a read too).
Now in the url add this: index.php?entryPoint=test&book_name=Art%20of%20War
Oh gawd that url encoding is hideous right! Don't worry about it.
You should hopefully get an ugly white screen with the text "exists". If you do let's make sure it also works the other way. Add a 2 to the book name in the url and hopefully it will now say "unique".
Quick note: if you're using Sugar you're probably also using mysql which is case insensitive when searching on strings. If you really need case sensitivity check out this SO article:
How can I make SQL case sensitive string comparison on MySQL?
Okay so now we have our entryPoint working and we can move on to the fun part of making everything all ajaxical. There are a couple ways to go about this, but rather than going the most basic route I'm gonna show you what I've found to be the most reliable route.
You probably will need to create the following file: custom/modules/CUSTOM_BOOK_MODULE/views/view.edit.php (I hope by now I don't need to point out changing that path to use your module name...
Assuming this file did not exist and we are starting from scratch here is what it will need to look like:
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class CUSTOM_BOOK_MODULEViewEdit extends ViewEdit
{
public function display()
{
// make sure it works in the subpanel too
$this->useForSubpanel = true;
// make the name value available in the tpl file
$this->ss->assign('name_value', $this->bean->name);
// load the parsed contents of the tpl into this var
$name_input_code = $this->ss->fetch('custom/modules/CUSTOM_BOOK_MODULE/tpls/unique_book_checker.tpl.js');
// pass the parsed contents down into the editviewdefs
$this->ss->assign('custom_name_code', $name_input_code);
// definitely need to call the parent method
parent::display();
}
}
Things are looking good. Now we gotta write the code in this file: custom/modules/CUSTOM_BOOK_MODULE/tpls/unique_book_checker.tpl.js
First a couple of assumptions:
We're going to expect that this is Sugar 6.5+ and jquery is already available. If you're on an earlier version you'll need to manually include jquery.
We're going to put the event listener on the name field. If the book name value that you want to check is actually a different field name then simply adjust that in the javascript below.
Here is the code for custom/modules/CUSTOM_BOOK_MODULE/unique_book_checker.tpl.js:
<input type="text" name="name" id="name" maxlength="255" value="{$name_value}" />
<span id="book_unique_result"></span>
{literal}
<script type="text/javascript">
$(document).ready(function() {
$('#name').blur(function(){
$('#book_unique_result').html('<strong> checking name...</strong>');
$.post('index.php?entryPoint=test', {book_name: $('#name').val(), book_id: $('[name="record"]').val()}, function(data){
if (data == 'exists') {
removeFromValidate('EditView', 'name');
addToValidate('EditView', 'name', 'float', true, 'Book Name Must be Unique.');
$('#book_unique_result').html('<strong style="color:red;"> ✗</strong>');
}
else if (data == 'unique') {
removeFromValidate('EditView', 'name');
addToValidate('EditView', 'name', '', true, 'Name Required');
$('#book_unique_result').html('<strong style="color:green;"> ✓</strong>');
}
else {
// uh oh! maybe you have php display errors on?
}
});
});
});
</script>
{/literal}
Another Note: When the code detects that the name already exists we get a little hacky and use Sugar's built in validation stuff to prevent the record from saving. Basically, we are saying that if the name already exists then the name value MUST be a float. I figured this is pretty unlikely and will do the trick. However if you have a book named 3.14 or something like that and you try to create a duplicate this code will NOT prevent the save. It will tell you that a duplicate was found, but it will not prevent the save.
Phew! Okay last two steps and they are easy.
First, open the file: custom/modules/CUSTOM_BOOK_MODULE/metadata/editviewdefs.php.
Next, find the section that provides the metadata for the name field and add this customCode attribute so that it looks like this:
array (
'name' => 'name',
'customCode' => '{$custom_name_code}',
),
Finally, you'll need to do a quick repair and rebuild for the metadata changes to take effect. Go to Admin > Repair > Quick Repair & Rebuild.
Boom! You should be good to go!
I'm using Basset 4 to manage assets.
In the config file i'm declaring a collection 'admin'
return array(
'collections' => array(
'admin' => function($collection)
{
$collection->directory('assets/js', function($collection)
{
$collection->add('vendor/jquery-1.9.1.min.js');
});
},
),
...
)
later in a view, I would like to add an extra file in admin collection.
I've try the following code, but it doesn't work:
Basset::collection('admin', function($collection)
{
$collection->add('function.js');
});
Is there a way to add file into a collection from a view or from a controller?
Thank you
Basset isn't really designed to work like that. You should be defining all your assets within that initial call, even though the ability to add assets throughout the execution of an application is possible, it's not recommended.
When building a collection assets that are added for a particular route won't be available to the builder since Artisan doesn't fire any routes, etc.
Adjusting a collection in numerous places can often lead to confusion further down the line.
I know this isn't ideal as you're probably looking at implementing page specific JavaScript, correct? I've thought about it but can't really think of a clean solution (suggestions?), although I've heard of people assigning a unique ID to the body or perhaps some classes that their JavaScript can then attach themselves to.
It's not brilliant but that's the best I can give you at the moment.