Unit test Laravel query builder class, mock problem - laravel

I'm trying to mock out the actual database operations in my Laravel application for the purposes of effective unit testing. But I am having difficulty in getting the mocked method to be called, rather than the real method.
I have about 200 Models in app/Models which were generated using artisan make:model. I'm in the process of creating query builder classes which extend Illuminate\Database\Eloquent\Builder for each of the models. Likewise, there will be a PHPUnit test class for each of these query builders.
I've spent the last 2 days with the Laravel docs, the Mockery docs, Stack Overflow, Google and lots of debugging trying to get my unit test to work correctly, to no avail. What's the trick to making this work with Laravel's intricate internals?
Example Model, stripped down:
<?php
namespace App\Models;
use App\Domain\Example\QueryBuilders\ExampleQueryBuilder;
use Illuminate\Database\Eloquent\Model;
/**
* #property int $id
* #property string $foo
* #property string $bar
* #property string $baz
*/
class Example extends Model
{
public $timestamps = false;
protected $table = 'my_table';
protected $fillable = ['foo', 'bar', 'baz',];
protected $dateFormat = 'U';
protected $connection = 'mysql';
public function newEloquentBuilder($query): ExampleQueryBuilder {
return new ExampleQueryBuilder($query);
}
}
Example query builder, stripped down:
<?php
namespace App\Domain\Example\QueryBuilders;
use App\Models\Example;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
final class ExampleQueryBuilder extends Builder
{
protected $model;
protected $onDelete;
protected $query;
public function findActive(string $foo): ?Model {
$conditions = ['foo' => "$foo", 'bar' => 1,];
$example = Example::firstWhere($conditions);
if ($example instanceof Example) {return $example;}
return null;
}
Example unit test:
<?php
namespace Tests\Domain\Example\QueryBuilders;
use App\Models\Example;
use Tests\TestCase;
class ExampleQueryBuilderTest extends TestCase
{
/** #test */
public function it_handles_missing_example_correctly(): void {
// I've tried endless variations on this, mocking the Model "Example",
// mocking "Model", even "Process" at one crazy point. Debugging proves
// without a doubt that the `Builder::firstWhere` method IS being called
// instead of my mocked method.
$mock = $this->partialMock(Builder::class, function (MockInterface $mock) {
$mock->shouldReceive('firstWhere')->once();
});
$this->app->instance(Builder::class, $mock);
$example = Example::findActive('');
self::assertNull($example);
}
}
When this test is run, I get this output:
Method firstWhere() from Mockery_0_Illuminate_Database_Eloquent_Builder should be called
exactly 1 times but called 0 times.
So clearly the method I need to mock out is not getting mocked somehow. What am I overlooking or missing?
My environment: Laravel 8, Mockery 1.4, PHPUnit 9, PHP 7.4/8.0, macOS/Linux

Related

How to mock laravel model relathipship?

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.

Laravel Mystery - Two Similar Item Types Producing 2 Different Query Strings in Same Use Case

Ok, this is weird... You ready?
I have an item type on my site, lets call it SomeItem
It can have tags associated with it via a one-to-many relationship.
The sorts of queries that Laravel builds when dealing with tags for SomeItem are like this, for instance in response to route api/someitem/10:
select `tags`.*, `someitem_tag`.`someitem_id` as `pivot_someitem_id`, `someitem_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `someitem_tag` on `tags`.`id` = `someitem_tag`.`tag_id` where `someitem_tag`.`someitem_id` in (10)
When I create a second Item with identical settings - let's call it AnotherItems - it treats the database query for extracting tags in a different manner, using a different syntax in the queries. Extremely weird.
(and yes, I have an s at the end of the model name...)
For instance, this route api/anotheritems/1
produces this error:
Base table or view not found: 1146 Table 'mysite.tag_anotheritems' doesn't exist (SQL: select `tags`.*, `tag_anotheritems`.`anotheritems_id` as `pivot_anotheritems_id`, `tag_anotheritems`.`tag_id` as `pivot_tag_id` from `tags` inner join `tag_anotheritems` on `tags`.`id` = `tag_anotheritems`.`tag_id` where `tag_anotheritems`.`anotheritems_id` in (1))
See what is happening? Of course I am getting this error - in the database this tag table for AnotherItems is created as anotheritems_tag. That is analogous to SomeItem.
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item??? WTF?
First let me show you how SomeItem is set up.
Here is the database structure related to Tags:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSomeItemTagTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('someitem_tag', function (Blueprint $table) {
$table->integer('tag_id')->unsigned();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->integer('someitem_id')->unsigned();
$table->foreign('someitem_id')->references('id')->on('someitems')->onDelete('cascade');
$table->primary(array('tag_id', 'someitem_id'));
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('someitem_tag');
}
}
There is a Tags model/class that has this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name'];
protected $hidden = [];
public $timestamps = false;
public function someitems()
{
return $this->belongsToMany(SomeItem::class);
}
}
And here is some relevant lines for SomeItem model/class:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Presenters\Presentable;
use Illuminate\Notifications\Notifiable;
use Auth;
class Exercise extends Model
implements Presentable
{
use Traits\SerializesUniversalDate;
use Traits\Presents;
use Notifiable;
protected $presenter = 'App\Presenters\SomeItemPresenter';
protected $fillable = ['title', etc];
protected $hidden = [];
public function parentitem()
{
return $this->belongsTo(ParentItem::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* Update lesson tag array.
*
* #param array \App\Tag $tags
* #return void
*/
public function updateTags($tagsArray)
{
foreach ($tagsArray as &$value)
{
$tag = Tag::where('name', $value['name'])->first();
if (is_null($tag))
{
$tag = new Tag([
'name' => $value['name']
]);
$tag->save();
}
if (!$this->tags->contains($tag->id))
{
$this->tags()->attach($tag->id);
}
}
foreach($this->tags as &$existingTag)
{
if (!self::arrayContains($tagsArray, 'name', $existingTag->name))
{
$this->tags()->detach($existingTag->id);
}
}
$this->load('tags');
}
private static function arrayContains($array, $key, $value)
{
foreach ($array as $item)
{
if($item[$key] == $value) return true;
}
return false;
}
}
And here is some relevant code for SomeItem API controller:
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Input;
class SomeItemController extends Controller
{
public function index(Request $request)
{
$query = \App\SomeItem::query();
return $query->get()->load('parentitem')->load('tags');
}
//show item for editing
public function show($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->load('parentitem')->load('tags');
$someitem->attachKindToFiles();
return $someitem;
}
//store new entry to db
public function store()
{
$someitem = \App\SomeItem::create(Input::all());
isset(Input::all()['tags']) ? $someitem->updateTags(Input::all()['tags']) : '';
return $someitem;
}
//update/save
public function update($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->update(Input::all());
$someitem->updateTags(Input::all()['tags']);
$someitem->load('tags');
return $someitem;
}
There is also a SomeItem presenter and composer but they don't do anything with tags.
With AnotherItems, I literally I duplicated everything from SomeItem and just changed names as needed.
So in the Tag model there is
public function anotheritems()
{
return $this->belongsToMany(AnotherItems::class);
}
In AnotherItems model there is this, for instance
public function tags()
{
return $this->belongsToMany(Tag::class);
}
In the AnotherItems API controller there is this, for instance (which is for route api/anotheritems/1):
public function index(Request $request)
{
$query = \App\AnotherItems::query();
if ($request->has('id')) {
$query->where('id', $request['id']);
}
return $query->get()->load('parentitem')->load('tags');
}
So, this is a total mystery. I have been trying to figure this out for 2 days now. And I continue asking myself
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item???
I upgraded from laravel 5.2 to 5.3 and it is after the upgrade that I added this AnotherItems. But I can't figure out how that could possibly alter things in terms of these database queries.
I have tried a ton of artisan commands for clearing everything imaginable, but somewhere in the framework it wants to handle SomeItem and AnotherItems differently when building these join queries to extract/save tags.
Thoughts?
thanks,
Brian
Decided to step through code in debugger. Seems things are breaking down in Str.php in various snake related function, and I also noticed a snakeCache call, whatever the heck that is. Not sure why such a strange methodology to determine table names... Also in these functions there is some pluralizing related checks, so maybe this is related to me using an s at the end of my item name. Pretty messed up stuff if an s at the end of a model name can cause two different logic branches...

Mocking class instantiation in a feature test

I'm currently working on my first Laravel project — a service endpoint that returns a resource based on a recording saved in S3. The service doesn't require a DB, but my idea was, I could keep the controller skinny, by moving the logic to a "model". I could then access the resource by mimic'ing some standard active record calls.
Functionally, the implementation works as expected, but I am having issues with mocking.
I am using a library to create signed CloudFront URLs, but it is accessed as a static method. When I first started writing my feature test, I found that I was unable to stub the static method. I tried class aliasing with Mockery, but with no luck — I was still hitting the static method. So, I tried wrapping the static method in a little class assuming mocking the class would be easier. Unfortunately, I'm experiencing the same issue. The thing that I am trying to mock is being hit as if I'm not mocking it.
This stack overflow post gives an example of how to use class aliasing, but I can't get it to work.
What is the difference between overload and alias in Mockery?
What am I doing wrong? I'd prefer to get mockery aliasing to work, but instance mocking would be fine. Please point me in the right direction.
 Thank you in advance for your help.
Controller
// app/Http/Controllers/API/V1/RecordingController.php
class RecordingController extends Controller {
public function show($id){
return json_encode(Recording::findOrFail($id));
}
}
Model
// app/Models/Recording.php
namespace App\Models;
use Mockery;
use Carbon\Carbon;
use CloudFrontUrlSigner;
use Storage;
use Illuminate\Support\Arr;
class Recording
{
public $id;
public $url;
private function __construct($array)
{
$this->id = $array['id'];
$this->url = $this->signedURL($array['filename']);
}
// imitates the behavior of the findOrFail function
public static function findOrFail($id): Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)) {
abort(404, "Recording not found with id $id");
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
// imitate the behavior of the find function
public static function find($id): ?Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)){
return null;
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
protected function signedURL($key) : string
{
$url = Storage::url($key);
$signedUrl = new cloudFrontSignedURL($url);
return $signedUrl->getUrl($url);
}
}
/**
* wrapper for static method for testing purposes
*/
class cloudFrontSignedURL {
protected $url;
public function __construct($url) {
$this->url = CloudFrontUrlSigner::sign($url);
}
public function getUrl($url) {
return $this->url;
}
}
Test
// tests/Feature/RecordingsTest.php
namespace Tests\Feature;
use Mockery;
use Faker;
use Tests\TestCase;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithFaker;
/* The following is what my test looked like when I wrapped CloudFrontUrlSigner
* in a class and attempted to mock the class
*/
class RecordingsTest extends TestCase
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL()
{
$recordingMock = \Mockery::mock(Recording::class);
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('getURL')
->once()
->andReturn($returnValue);
$this->app->instance(Recording::class, $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
// The following is what my test looked like when I was trying to alias CloudFrontUrlSigner
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL1()
{
$urlMock = \Mockery::mock('alias:Dreamonkey\cloudFrontSignedURL');
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('sign')
->once()
->andReturn($returnValue);
$this->app->instance('Dreamonkey\cloudFrontSignedURL', $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
phpunit
$ phpunit tests/Feature/RecordingsTest.php --verbose
...
There was 1 failure:
1) Tests\Feature\RecordingsTest::if_a_recording_exists_with_provided_id_it_will_return_a_URL
Expected status code 200 but received 500.
Failed asserting that false is true.
/Users/stevereilly/Projects/media-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/Users/stevereilly/Projects/media-service/tests/Feature/RecordingsTest.php:85
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:206
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:162
You're getting a 500, which means there's something wrong with the code. Just by scanning it I notice you're missing a filenameFromId method on the Recordings class, and the Test is creating a mock named $recordingMock, but you try to use $urlMock. Try to fix those issues first.
Then you're mocking the class, but you never replace it in your application (you did it in the old test apparently).
Generally you want to follow these steps when mocking:
1. Mock a class
2. Tell Laravel to replace the class with your mock whenever someone requests it
3. Make some assertions against the mock

Extending the Laravel Eloquent\Collection Class

I understand that the default Eloquent\Collection class can be overridden in your model by using the method:
public function newCollection(array $models = array()) {
return new CustomCollection($models);
}
Which works great if I'm using typical queries such as:
Model::where('name', $name)->get();
This is great so I can add methods to the eloquent collection class, such as:
$records = Model::where('name', $name)->get();
$records->toTable();
But if I'm using pagination on the model, for example:
Model::where('name', $name)->paginate(25);
It returns an instance of the class Illuminate\Support\Collection instead of the Illuminate\Database\Eloquent\Collection.
Is there a way of overriding or extending the typical Illuminate\Support\Collection?
I'm trying to add a toTable() method to the returned Collection. I'd rather not have to replace the pagination service provider with my own.
Thanks!!
You will need to replace the pagination service provider, amongst a couple of other classes in the pagination library. By the sound of it you know how to do it this way, but were hoping for another answer, but as I have the code I'll drop it in here for you.
The reason you need to replace these classes/methods is because the files in Illuminate directly reference instances of classes within the Illuminate namespace.
In config/app.php
Replace
'Illuminate\Pagination\PaginationServiceProvider',
With
'ExtendedPaginationServiceProvider',
Create a new file somewhere the autoloader is capable of finding it called ExtendedPaginationServiceProvider.php and place the following in it
<?php
use Illuminate\Support\ServiceProvider;
class ExtendedPaginationServiceProvider extends ServiceProvider
{
/**
* #inheritdoc
*/
public function register()
{
$this->app->bindShared('paginator', function($app)
{
$paginator = new ExtendedPaginationFactory($app['request'], $app['view'], $app['translator']);
$paginator->setViewName($app['config']['view.pagination']);
$app->refresh('request', $paginator, 'setRequest');
return $paginator;
});
}
}
Create a new file somewhere the autoloader is capable of finding it called ExtendedPaginationFactory.php and place the following in it
<?php
use Illuminate\Pagination\Factory;
class ExtendedPaginationFactory extends Factory
{
/**
* #inheritdoc
*/
public function make(array $items, $total, $perPage = null)
{
$paginator = new ExtendedPaginationPaginator($this, $items, $total, $perPage);
return $paginator->setupPaginationContext();
}
}
Create a new file somewhere the autoloader is capable of finding it called ExtendedPaginationPaginator.php and place the following in it
<?php
use Illuminate\Pagination\Paginator;
class ExtendedPaginationPaginator extends Paginator
{
/**
* Get a collection instance containing the items.
*
* #return ExtendedCollection
*/
public function getCollection()
{
return new ExtendedCollection($this->items);
}
}
You'll notice the above returns a new instance of ExtendedCollection. Obviously replace this with your CustomCollection class you refer to in your question.
For others to reference, an ExtendedCollection class may look similar to the below
Create a new file somewhere the autoloader is capable of finding it called ExtendedCollection.php and place the following in it
<?php
use Illuminate\Support\Collection;
class ExtendedCollection extends Collection
{
}
Also, after creating these files, don't forget to run the following in the terminal
composer dump-autoload

phpunit not finding model method (Laravel / Mockery)

I'm getting started with unit testing in Laravel 4, and I'm stuck testing a custom method in a model I've added to the standard User model.
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends BaseModel implements UserInterface, RemindableInterface {
/**
* Logs to the database and streams an update
* Uses logIt and streamIt on Base model
* #param String $action The action being performed
* #return void
*/
private function logAndStream($action)
{
$this->logIt('info', $action.'d user '.$this->username);
$this->streamIt($action.'d user '.$this->username);
}
This class extends the BaseModel which in turn extends Eloquent, and has the defines the logIt and StreamIt methods like so:
class BaseModel extends Eloquent {
/**
* Log an action to log file
* #return void
*/
protected function logIt($level, $msg) {
...
}
/**
* Log an action to activity stream
* #return void
*/
protected function streamIt($msg, $client = null, $project = null) {
...
}
All of this code works fine when I'm manually testing things. But now I want to create a unit test to automate it.
class UserTest extends TestCase {
public function testLogAndStream()
{
$base = Mockery::mock('BaseModel')->shouldAllowMockingProtectedMethods();
$base->shouldReceive('logIt')
->with('info', 'Created user Tester')
->once();
$user = new User;
$user->username = 'Tester';
$user->logAndStream('Create');
}
When I try running this, I get a failure complaining about not finding logAndStream.
1) UserTest::testLogAndStream
BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::logAndStream()
What am I missing?
You have two problems here:
First, your User model contains a logAndStream method, but it's private. This means that doing this:
$user->logAndStream('Create');
is not possible because only public methods are accessible in this way.
Second, in your test, you are mocking an instance of BaseModel. This has nothing to do with the instance of User that you instantiate a couple of lines later. User extends BaseModel - you can still have instances of User and BaseModel that are not related to one another. Here's an analogy:
class Database {
}
class DatabaseWithExtraFeatures extends Database {
}
The first one (Database) is just a plain old database access class and works just fine for basic stuff. Then someone comes along and realizes that Database doesn't provide some extra feature, so they build upon it by extending it. A developer can use either, or even both, in the same application.
$db = new Database;
$dbExtra = new DatabaseWithExtraFeatures;
// do something with $db
$result1 = $db->query();
// do something with $dbExtra
$result2 = $dbExtra->extraSpecialQuery();
What you've done in your test is analogous to this - you've mocked one instance of BaseModel and then instantiated a User class (which just happens to extend BaseModel).
EDIT Here's a little more detail on how to mock the user model:
$user = Mockery::mock('User')->shouldAllowMockingProtectedMethods();
$user->shouldReceive('logIt')
->with('some arguments')
->once();
$user->shouldReceive('streamIt')
->with('some arguments')
->once();
// when you set a property on an Eloquent model, it actually calls the
// setAttribute method. So do this instead of $user->username = 'Tester'
//
$user->shouldReceive('setAttribute')
->with('username', 'Tester')
->once()
$user->logAndStream('Create');
// now assert something...
Because User inherits from BaseModel, the methods from BaseModel are actually methods on User and can be treated as if they were defined as part of User.
Disclaimer: This was extracted from the question.
Firstly, I should have been checking that logIt and streamIt where observed on the same, mocked User model and not the parent BaseModel.
Populating the mock user model with $user->username was not correct either. In the end, Kryten helped me realise that Eloquent internally calls getAttribute('username') for this anyway, so I can return the value directly as part of an assertion, which would then be fed into logIt and StreamIt. This works but feels a little clunky - if anybody can suggest a better way, I'd love to learn.
Here's the working test case which works irrespective of whether logAndStream is declared as either a public or protected:
public function testLogAndStream()
{
$user = Mockery::mock('User');
$user->shouldReceive('getAttribute')
->with('username')
->atLeast()->once()
->andReturn('Tester');
$user->shouldReceive('logIt')
->once()
->with('info','Created user Tester');
$user->shouldReceive('streamIt')
->once()
->with('Created user Tester');
$user->logAndStream('Create');
}

Resources