I'm trying to write test for this repo: laravel.com. below is the app's structure.
App\Documentation.php
public function __construct(Filesystem $files, Cache $cache)
{
$this->files = $files;
$this->cache = $cache;
}
public function get($version, $page)
{
return $this->cache->remember('docs.'.$version.'.'.$page, 5, function () use ($version, $page) {
if ($this->files->exists($path = $this->markdownPath($version, $page))) {
return $this->replaceLinks($version, markdown($this->files->get($path)));
}
return null;
});
}
public function markdownPath($version, $page)
{
return base_path('resources/docs/'.$version.'/'.$page.'.md');
}
DocsController.php
public function __construct(Documentation $docs)
{
$this->docs = $docs;
}
public function show($version, $page = null)
{
$content = $this->docs->get($version, $sectionPage);
}
here is my test logic
$mock = Mockery::mock('App\Documentation')->makePartial();
$mock->shouldReceive('markdownPath')->once()->andReturn(base_path('test/stubs/stub.md'));
$this->app->instance('App\Documentation', $mock);
$this->get('docs/'.DEFAULT_VERSION.'/stub') //It hits DocsController#show
//...
here is the error
Call to a member function remember() on null
Different ways I've tried so far and different errors
1)
$mock = \Mockery::mock('App\Documentation');
$mock->shouldReceive('markdownPath')->once()->andReturn(base_path('test/stubs/stub.md'));
app()->instance('App\Documentation', $mock);
Mockery\Exception\BadMethodCallException: Received Mockery_0_App_Documentation::get(), but no expectations were specified
Here I don't wanna define expectation for each methods.
2)
app()->instance('App\Documentation', \Mockery::mock('App\Documentation[markdownPath]', function ($mock) {
$mock->shouldReceive('markdownPath')->once()->andReturn(base_path('test/stubs/stub.md'));
}));
ArgumentCountError: Too few arguments to function App\Documentation::__construct(), 0 passed and exactly 2 expected
If we specify method name in mock (App\Documentation[markdownPath]); the constructor is not mocking, so we get error.
You can use the container to resolve a real instance (so that the filesystem and cache dependencies are resolved), and then create a partial mock from your real instance in order to define the expectation for the markdownPath method. Once your mock instance is setup, put it into the container for the controller to use.
// Resolve a real instance from the container.
$instance = $this->app->make(\App\Documentation::class);
// Create a partial mock from the real instance.
$mock = \Mockery::mock($instance);
// Define your mock expectations.
$mock->shouldReceive('markdownPath')->once()->andReturn(base_path('test/stubs/stub.md'));
// Put your mock instance in the container.
$this->app->instance(\App\Documentation::class, $mock);
Related
I use the map function to loop each object of collections.
The closure method is too long and I tried to make it a private function.
But I cannot successfully call the function.
Here is my sample code. (note: the function test may be very long in real case)
<?php
class cls
{
private function test($a) {
return ($a + 1);
}
public function run1() {
return ($this->test(5));
}
public function run2() {
$col = Collect([1,2,3]);
return ($col->map($this->test()));
}
public function run3() {
$col = Collect([1,2,3]);
$mycls = $this;
return ($col->map(function ($c) use ($mycls) {
return($mycls->test($c));
}));
}
}
$c = new cls;
$c->run1(); # 6
$c->run2(); # Error: Too Few Argements
$c->run3(); # [2,3,4]
I use the function: run1 to test if the private function is callable and I failed at function: run2.
Although function: run3 makes code shorten. It seems a little bit too superfluous.
How can I make run2 works?
Update
My version of Laravel is 6.2
Update
I tried the #xenooooo answer.
It works with the public function, but I get a different error code with the private method.
Simple answer: $this->test() required a parameter and you are simply not passing anything to it.
You can also modify your run2/run3 methods to do the following:
public function run2() {
$col = collect([1,2,3])->map(function ($value) {
return $this->test($value);
});
return $col; //returns a collection of items
//return $col->toArray(); //(extra option) returns an array of collected items
}
Result:
Illuminate\Support\Collection {#872 ▼
#items: array:3 [▼
0 => 2
1 => 3
2 => 4
]
#escapeWhenCastingToString: false
}
You may read more on collections here: https://laravel.com/docs/9.x/collections#introduction
The problem in run2 is that you are calling the method directly and you need to pass the an argument since test is expecting argument $a. To use the method as a callback function you need the pass it as an array which is the first value is $this and the second is the method name which is test :
public function test($a) {
return ($a + 1);
}
public function run2()
{
$collection = collect([1,2,3]);
$newCollection = $collection->map([$this,'test']);
return $newCollection;
}
UPDATE
It only work if the method that you will call is public and it will not work if you use a private or protected method. If you use a private or protected if will throw a BadMethodCallException
I have this test in my Laravel project:
// here are some test function configurations
// found nothing if not match
$this->post(action([TagController::class, 'search'], '!! Not foundable name !!'))
->assertOk()
->assertJsonCount(0, 'data');
// found it if match
$this->post(action([TagController::class, 'search'], $matchName))
->assertOk()
->assertJsonCount(1, 'data');
// found it if partly match
$this->post(action([TagController::class, 'search'], $partlyMatchName))
->assertOk()
->assertJsonCount(1, 'data');
Now I see this result if test failed:
Failed to assert that the response count matched the expected 0
Failed asserting that actual size 4 matches expected size 0.
This isn't say too mutch for me, I don't see which assertation failed and exactly why. I want to define custom message for this case.
I want to do someting like this:
->assertJsonCount(
0,
'data',
'It should found noting, because of conditions are not match'
);
Is there any way to send custom message to the tester user in this case?
You should override decodeResponseJson() method of Illuminate\Testing\TestReponse class.
Create two classes of TestResponse and AssertableJsonString in Tests namespace as follows:
TestResponse
namespace Tests;
use Illuminate\Testing\Assert as PHPUnit;
class TestResponse extends \Illuminate\Testing\TestResponse
{
/**
* #inheritDoc
*/
public function decodeResponseJson()
{
$testJson = new AssertableJsonString($this->getContent());
$decodedResponse = $testJson->json();
if (is_null($decodedResponse) || $decodedResponse === false) {
if ($this->exception) {
throw $this->exception;
} else {
PHPUnit::fail('Invalid JSON was returned from the route.');
}
}
return $testJson;
}
}
AssertableJsonString
namespace Tests;
use Illuminate\Testing\Assert as PHPUnit;
class AssertableJsonString extends \Illuminate\Testing\AssertableJsonString implements \ArrayAccess, \Countable
{
/**
* #inheritDoc
*/
public function assertCount(int $count, $key = null, $message="Failed to assert that the response count matched the expected %d")
{
if (! is_null($key)) {
PHPUnit::assertCount(
$count, data_get($this->decoded, $key),
sprintf($message, $count)
);
return $this;
}
PHPUnit::assertCount($count,
$this->decoded,
sprintf($message, $count)
);
return $this;
}
}
Now, you need to bind Laravel TestResponse class to your custom TestResponse in boot method of AppServiceProvider as follows:
public function boot()
{
$this->app->bind(\Illuminate\Testing\TestResponse::class,\Tests\TestResponse::class);
}
Notice: You need to place %d in your message format to be replaced by sprintf() function.
I'm missing something with how the global scopes work in Laravel 5.5.
In my controller, index , I am passing filters into a getter:
public function index(SaleFilters $filters)
{
return new SaleCollection($this->getSales($filters));
}
getSales:
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}
protected function range($range)
{
$dates = explode(" ", $range);
if (count($dates) == 2) {
$this->builder = Sale::with(['office', 'staff'])
->where('sale_date', '>=', $dates[0])
->where('sale_date', '<', $dates[1])
->orderBy('sale_date', 'desc');
return $this->builder;
}
return false;
}
I have a scope setup in the sale model as such, which I would have thought would apply to the above filter automatically ? If not, do I have to reapply the same scope, duplicating the scope code in the filter ?
protected static function boot()
{
parent::boot();
$user = Auth::user();
if (($user) && ($user['office_id'])) {
return Sale::ofOffice($user['office_id'])->get();
}
}
public function scopeOfOffice($query, $office)
{
return $query->where('office_id', $office);
}
So basically, IF the user has an office_id applied, it should apply the ofOffice scope, therefore it should only ever return the sales that apply to that office_id.
Basically it works on page load via axios GET request
Route::get('/sales', 'SalesController#index')->middleware('auth:api');
axios
.get('api/sales/?range=" + this.rangeFilter)
rangeFilter is basically a start and end date passed into the above filter query.
Can anyone shed some light on how the scopes really work or if anything is obvious as to why its not always working? As I said, it works on page load where I provide default values for the rangeFilter, however when I change those days and it refetches via the same axios call, it seems to not be applying the scope, and I get ALL results instead of where office_id = 'x'
As far as i'm concerned, the range filter above would be executing on the first page load as well, so not sure why it would apply there, and not afterwards.
You should not mix the use of dynamic scope with global one. Also, static boot function does not expect a return. In order to use dynamic scope, you need to call it every time you need it. Hence, the name is dynamic. Query applied is not always executed by default. There so,
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice($anyOfficeHere)->with('office')->filter($filters);
return $sales->paginate(50);
}
To suit your existing code, you may want to add an if statement in your model. Then call the scope function without argument.
public function scopeOfOffice($q)
{
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
}
// Your controller
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice()->with('office')->filter($filters);
return $sales->paginate(50);
}
If you feel so much cumbersome to type ofOffice repeatedly. A global scope is the way to go. Within your model static boot function, you can also apply anonymous function if you feel creating a separated class kinda bloat your apps.
protected static function boot()
{
parent::boot();
static::addGlobalScope('officeOrWhatNot', function ($q) {
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
});
}
// Your controller. No more `ofOffice`, it's automatically applied.
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}
Accessors will do their job on a single attribute perfectly, but I need a way to have a method to do an Accessor/Getter job on all attributes and automatically.
The purpose is that I want to replace some characters/numbers on getting attributes and then printing them out. I can do it from within controller and manually but I think it would be great to have it from model side and automatically.
Like overriding getAttributes() method:
public function getAttributes()
{
foreach ($this->attributes as $key => $value) {
$this->attributes[$key] = str_replace([...], [...], $value);
}
return $this->attributes;
}
But I have to call it every time on model $model->getAttributes();
Any way to do it automatically and DRY?
Try something like:
public function getAttribute($key)
{
if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
if($key === 'name') return 'modify this value';
return $this->getAttributeValue($key);
}
return $this->getRelationValue($key);
}
It's fully overriding the default method so be a bit careful.
EDIT
Also check out: http://laravel.com/docs/5.1/eloquent-mutators
I would go with following approach and override the models __get method:
public function __get($key)
{
$excluded = [
// here you should add primary or foreign keys and other values,
// that should not be touched.
// $alternatively define an $included array to whitelist values
'foreignkey',
];
// if mutator is defined for an attribute it has precedence.
if(array_key_exists($key, $this->attributes)
&& ! $this->hasGetMutator($key) && ! in_array($key, $excluded)) {
return "modified string";
}
// let everything else handle the Model class itself
return parent::__get($key);
}
}
How about running it with each Creating and Updating events. So you can do something like that:
public function boot()
{
Model::creating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
Model::updating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
}
I'm a bit confused how I am to add methods to Eloquent models. Here is the code in my controller:
public function show($id)
{
$limit = Input::get('limit', false);
try {
if ($this->isExpand('posts')) {
$user = User::with(['posts' => function($query) {
$query->active()->ordered();
}])->findByIdOrUsernameOrFail($id);
} else {
$user = User::findByIdOrUsernameOrFail($id);
}
$userTransformed = $this->userTransformer->transform($user);
} catch (ModelNotFoundException $e) {
return $this->respondNotFound('User does not exist');
}
return $this->respond([
'item' => $userTransformed
]);
}
And the code in the User model:
public static function findByIdOrUsernameOrFail($id, $columns = array('*')) {
if (is_int($id)) return static::findOrFail($id, $columns);
if ( ! is_null($user = static::whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
So essentially I'm trying to allow the user to be retrieved by either user_id or username. I want to preserve the power of findOrFail() by creating my own method which checks the $id for an int or string.
When I am retrieving the User alone, it works with no problem. When I expand the posts then I get the error:
Call to undefined method
Illuminate\Database\Query\Builder::findByIdOrUsernameOrFail()
I'm not sure how I would go about approaching this problem.
You are trying to call your method in a static and a non-static context, which won't work. To accomplish what you want without duplicating code, you can make use of Query Scopes.
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
if ( ! is_null($user = $query->whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
You can use it exactly in the way you are trying to now.
Also, you can use firstOrFail:
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
return $query->whereUsername($id)->firstOrFail($columns);
}
Your method is fine, but you're trying to use it in two conflicting ways. The one that works as you intended is the one in the else clause, like you realised.
The reason the first mention doesn't work is because of two things:
You wrote the method as a static method, meaning that you don't call it on an instantiated object. In other words: User::someStaticMethod() works, but $user->someStaticMethod() doesn't.
The code User::with(...) returns an Eloquent query Builder object. This object can't call your static method.
Unfortunately, you'll either have to duplicate the functionality or circumvent it someway. Personally, I'd probably create a user repository with a non-static method to chain from. Another option is to create a static method on the User model that starts the chaining and calls the static method from there.
Edit: Lukas's suggestion of using a scope is of course by far the best option. I did not consider that it would work in this situation.