Imagine you have a controller or some code that looks something like this:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TeamController {
public function create(Request $request): JsonResponse
{
$request->validated();
$team = new Team(['name' => $request->get('name')]);
if (!$team->save()) {
// some really important logic you want to cover
// cover by a test.
}
...
}
}
You cannot easily mock the Eloquent model without being a bit messy, see my answer on how to return false from the save() call.
In your test simply call:
Team::saving(fn () => false);
and your call to save() will now return false allowing you to cover that code flow.
Related
I have a model that has a relationship with a View, that is complicate to popolate for make the feature test, but in the same time this is called from some component that are inside the controller called.
The following code is an example:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\TemperatureView;
class Town extends Model
{
function temperature()
{
return $this->hasOne(TemperatureView::class);
}
}
This is an example of the controller:
<?php
namespace App\Http\Controllers;
use App\Models\Town;
class TownController extends Controller
{
public function update($id)
{
// Here is the validation and update of Town model
$UpdatedTown = Town::where('id',$id);
$UpdatedTown->update($data);
$this->someOperation($UpdatedTown);
}
private function someOperation($Town)
{
//Here there is some operation that use the temperature Relationship
/*
Example:
$Town->temperature->value;
*/
}
}
The test is like is like this:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
use App\Models\TownModel;
use Mockery;
use Mockery\MockInterface;
class TownTest extends TestCase
{
/**
* A basic test example.
*
* #return void
*/
public function test_get_town_temperature()
{
$payload = ['someTownInformation' => 'Value'];
$response = $this->post('/Town/'.$idTown,$payload);
$response->assertStatus(200);
//This test failed
}
public function test_get_town_temperature_with_mocking()
{
$this->instance(
TownModel::class,
Mockery::mock(TownModel::class, function (MockInterface $mock) {
$MockDataTemperature = (object) array('value'=>2);
$mock->shouldReceive('temperature')->andReturn($MockDataTemperature);
})
);
$payload = ['someTownInformation' => 'Value'];
$response = $this->post('/Town/'.$idTown,$payload);
$response->assertStatus(200);
//This test also failed
}
}
The first test failed because the Controller has some check on the relationship temperature, that is empty because the view on database is empty.
The second test failed also for the same reason. I tried to follow some others questions with the official guide of Laravel Mocking. I know this is mocking object and not specially Eloquent.
Is something I'm not setting well?
If it's not possible to mock only the function, is possible to mock all the relationship of view, bypassing the DB access to that?
Edit
I undestand that the mocking work only when the class is injected from laravel, so what I wrote above it's not pratical.
I don't know if it's possible to mock only it, I saw a different option, that to create the interface of the model and change for the test, but I didn't want to make it.
I have two controllers. StudentController and TeacherController. I have a variable $chat which I want to pass in all the views of StudentController and TeacherController. The $chat will contain different data for both these controllers.
I searched and found ways, but I am getting empty data. I am doing it like this.
<?php
namespace App\Http\Controllers;
use View;
class StudentController extends Controller {
public function __construct()
{
$this->middleware('auth')->except(['home']);
$this->middleware('access')->except(['home']);
$chats = studentChat();
View::share('chats', $chats);
}
So, here I am printing and it is returning an empty array, but when I use the same in a function the array contains data. What is wrong here? Can anyone please help?
What I tried:
public function boot()
{
View::composer('*', function ($view) {
$chats = Cache::remember('chats', 60, function () {
if(Auth::user()->user_type() == config('constant.student'))
{
return studentChat();
}
else
{
return teacherChat();
}
});
$view->with('chats', $chats);
});
}
If you use View::share your share data to ALL your view, if you need to add data to few different views you may do this:
Create blade file(chat.blade.php for your case), and put your variables:
<? $chats = studentChat(); ?>
Include this file to the begining of your views where your need this 'global' varables:
//begin of your blade file
#include('chat')
//some code
{{ $chat->channel }}
Sharing Data With All Views
Occasionally, you may need to share a piece of data with all views that are rendered by your application. You may do so using the view facade's share method. Typically, you should place calls to share within a service provider's boot method. You are free to add them to the AppServiceProvider or generate a separate service provider to house them:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\View;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
$chats = studentChat();
View::share('chats', $chats);
}
public function register()
{
//
}
}
So, what I did was in the AppServiceProvider class, in the boot function I added this.
View::composer('*', function ($view) {
if(!\Auth::check())
{
return;
}
$userType = \Auth::user()->user_type ;
if($userType == config('constant.student'))
{
$chats = studentChat();
}
else if($userType == config('constant.teacher'))
{
$chats = teacherChat();
}
$view->with('chats', $chats);
});
You can pass data to the view Like.
return View::make('demo')->with('posts', $posts);
For more details visit article : Introduction to Routing in Laravel
write your query in boot method in appServiceProvider like,
View::composer('*', function ($view) {
$share_query = Cache::remember('share_query', 60,function () {
return App\User::all();
});
$view->with('share_query', $share_query);
});
Your final solution is ok, but not the cleanest possible.
Here is what i would do.
Define a class with a single function that contains your logic and return $chats, that way you will encapsulate your logic properly and keep your service provider boot method clean.
Then you have 2 options:
Inject your class in the boot() method of the service provider you use, then call its function and uses View::share. Should looks like :
public function boot(ChatResolver $chatResolver)
{
$chats = $chatResolver->getChats();
View::share(compact('chats));
}
If you only use $chats variable in a signe view or partial (like a part of layout), you can also inject the class you defined directly in the view.
Here is a link to Laravel doc regarding that.
In some cases it might be the easiest solution.
I'm trying to mock (it's example only) $user->posts()->get().
example service:
use App\Models\User;
class SomeClass{
public function getActivePost(User $user): Collection
{
return $user->posts()->get();
}
}
and my Model:
and Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use \App\Models\Post;
class User extends Model
{
public function posts() : HasMany
{
return $this->hasMany(Post::class);
}
}
this doesn't work:
$this->user = Mockery::mock(User::class);
$this->user
->shouldReceive('wallets->get')
->andReturn('test output');
error:
TypeError: Return value of Mockery_2_App_Models_User::posts() must be an instance of Illuminate\Database\Eloquent\Relations\HasMany, instance of Mockery_4__demeter_posts returned
without return type hint (on post() method) everything is ok. Must I modify andReturn()? idk how
This error can be solved by using the alias prefix with a valid class name. Like the following:
$m = m::mock('alias:App\Models\User');
More information can be found at the official documentation http://docs.mockery.io/en/latest/reference/creating_test_doubles.html#aliasing
Alternatively you can use like this.
use App\Models\User;
class SomeClass{
public function getActivePost(User $user): Collection
{
$user->load('posts');
return $user->posts;
}
}
First you need to mock post, then add it to Collection (don't forget to use it in the top). Then when you call posts attribute its takes mocked $posts. In this case it will not throw error about return type.
use Illuminate\Database\Eloquent\Collection;
$post = $this->mock(Post::class)->makePartial();
$posts = new Collection([$post]);
$this->user = Mockery::mock(User::class);
$this->user
->shouldReceive('getAttribute')
->with('posts');
->andReturn($posts);
Also i wouldn't use mocks here. There is absolutely no need for it. So the unit test i write would be:
Create a user.
Create some posts authored by the user.
Perform assertions on user & posts.
So the code will then be something like this in my test:
$user = factory(User::class)->create();
$posts = factory(Post::class, 5)->create(['user_id' => $user->id]);
$this->assertNotEmpty($user->id);
$this->assertNotEmpty($posts);
$this->assertEquals(5, $posts->fresh()->count());
$this->assertEquals($user->id, $post->fresh()->first()->user_id);
if you want to test the relationship you can:
/** #test */
function user_has_many_posts()
{
$user = factory(User::class)->create();
$post= factory(Post::class)->create(['user_id' => $user->id]);
//Check if database has the post..
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'user_id' => $user->id,
]);
//Check if relationship returns collection..
$this->assertInstanceOf('\Illuminate\Database\Eloquent\Collection', $user->posts);
}
I have to build an api.
It has one route. The client is sending a POST request with an XML.
Based on that xml, I have to decide witch controller#action to be called.
And I have a lot of controllers.
Unfortunately I can't modify the client side.
Do you have any suggestion how can i do that in a Laravel way?
For example
POST["body"] =
"...
<controller>content</controller>
<action>index</action>
..."
I want to call a ContentController::index()
Thx!
Thx for the reflection stuff. It is a big magic, worth the effort to look into it deeper.
I have no problem to parse the xml. So here is a simplier example
URL: /api/request/content/show
Routes.php
Route::get('api/request/{controller}/{action}', 'ApiController#request');
ApiController.php
class ApiController extends Controller
{
public function request($controller, $action)
{
//some error check
$controller = 'App\Http\Controllers\\' . ucfirst($controller) . 'Controller';
$params = "Put some params here";
$reflectionMethod = new \ReflectionMethod($controller, $action);
$reflectionMethod->invoke(new $controller, $params);
}
}
ContentController.php
class ContentController extends Controller
{
public function show($params)
{
dd($params);
}
}
And it is working!
Thx a lot!
A better option is to use App::call(); invoking controller with ReflectionMethod might not let you use response() inside your new forwarded controller and other laravel goodies.
Here is my try on this: /api/request/content/show
Routes web.php or api.php
use App\Http\Controllers\ApiController;
Route::get('api/request/{controller}/{action}', [ApiController::class, 'request']);
ApiController.php
class ApiController extends Controller
{
public function request($controller, $action)
{
//some error check
return App::call('App\Http\Controllers\\'.ucfirst($controller).'Controller#'.$action);
}
}
ContentController.php
use Illuminate\Http\Request;
class ContentController extends Controller
{
public function show(Request $request)
{
dd($request);
}
}
This will allow you more freedom.
Following is my routes where I am calling my Controller directly in route.
How can I pass Request $request to my getBlog Function.. Or is there any way to directly get $request object in my getblog function of controller ???
$artObj = App::make('App\Http\Controllers\Front\ArticleController');
return $artObj->getBlog($id);
Code in Routes:
Route::get('/{slug}', function($slug) {
// Get Id and Type depending on url
$resultarray = App\Model\UrlAlias::getTypefromUrl($slug);
if(!empty($resultarray)) {
if($resultarray[0]['type'] == 'article') {
$id = $resultarray[0]['ref_id'] ;
$artObj = App::make('App\Http\Controllers\Front\ArticleController');
return $artObj->getBlog($id);
} else {
return Response::view("errors.404", $msg, 400);
}
} else {
return Response::view("errors.404", array(), 400);
}
});
You can do in the head of the routes.php file:
use Illuminate\Http\Request;
And then in the beginning of your route:
Route::get('/{slug}', function($slug, Request $request) {
And the $request will be available to you. But that is extremely bad practice. What you should do - is move the whole logic into the controller like that:
Route::group(['namespace' => 'Front'], function(){
Route::get('/{slug}', 'ArticleController#methodName');
});
and then you can use the Request class in your controller class and the controller method:
<?php
namespace App\Http\Controllers\Front
use Illuminate\Http\Request;
class ArticleController
{ ...
public function methodName(Request $request){
...function implementation...
}
...
}
The Request is a global variable, you can access it anywhere with either php code or even some helper Laravel functions.
Just use request() and it's the same as passing the request as an object inside a variable through a function. (It's equivalent to the Request $request variable received).
It improved readability also. Just remember you can't change request objects directly, you'd better use request()->merge(['key' => 'newValue']);