Mock out function call to another function in same class - laravel

I have function that calls out to another function in the same class. I'm trying to test if that method is called in my unit tests however can't find a way to mock it.
Here is an example of the method call
public function follow($user_id, $followsfeed, $target_user_id)
{
$news_feeds = $this->getNewsFeeds($user_id);
}
I want to mock the method getNewsFeed and assert it is being called when I execute follow.
What is the best way to achieve this. as I can't workout how to create an internal mocked object.

You can use a partial mocks, as example on your test class, you can do:
public function test_follow()
{
$mock = Mockery::mock('App\Services\SomeService')->makePartial();
$mock->shouldReceive('getNewsFeeds')
->once()
->with("id1","id2")
->andReturn($someNews);
$mock-> follow("id1","id2");
}
You can also use the PHPUnit test-doubles
Hope this help

Related

Is the code actually executed when using a Mockery spy?

I am trying to write some tests with a Mockery Spy. However, it doesn't seem like the "spied upon" code is executed when I'm using a spy.
Is the code actually executed when using a Mockery spy?
Here is what I tested:
// In my test:
$spy = $this->spy(FeedManager::class);
// Controller
resolve(FeedManager::class)->createResponse();
// FeedManger::createResponse()
public static function createResponse(Builder $builder)
{
dd("here i am"); // this never gets called unless I remove the spy
}
Apparently, the "spy code" is not supposed to run. Found this quote from the docs.
The \Mockery::spy() method call is actually a shorthand for calling
\Mockery::mock()->shouldIgnoreMissing(). The shouldIgnoreMissing
method is a “behaviour modifier”.

Laravel eloquent: How does relation inherit functions from query builder?

how do they achieve, that Illuminate\Database\Eloquent\Relations\Relation has access to all query builder functions?
I see they have a $query property, but does not explain how all its methods are available inside relation
If you are aware of php magic methods then you will know __call method
this method will be called when you initialize a php object and you try to call a method which is not available in the class. By using __call method from the Illuminate\Database\Eloquent\Relations\Relation class they are forwarding the call to Illuminate\Database\Eloquent\Builder. I will explain it very clearly by pointing out the code.
Inside the laravel framework there is a trait named as ForwardsCalls . This trait is used in many classes to handle the call forwarding to another class.
So here is how the call from the Relation class is forwarded to Builder class. While initilting the new Relation class Builder class will be initialized. So when you try to call a method from reltion class which is not available it will call __call method. After that it will look for a available macros . So when a macros method is not found. Then it will use forwardDecoratedCallTo from ForwardsCalls Trait.
So forwardDecoratedCallTo will accept 3 arguments namely $object, $method and $parameters. Whereas
$object will be $this->query which has a Illuminate\Database\Eloquent\Builder instance.
$method will be the method the you try to access from Builder Method.
$parameters will be the all the parameters that is be passed to the method.
I will try to Demonstrate will the example without the traits and helpers from laravel
class ClassTwo {
public function classTwoMethodOne()
{
dd(__FUNCTION__.' has been called');
}
public function classTwoMethodTwo()
{
dd(__FUNCTION__.' has been called');
}
public function classTwoMethodThree()
{
//you cannot call this method dynamically
dd(__FUNCTION__.' has been called');
}
}
class ClassOne {
public function classOneMethodOne()
{
dd(__FUNCTION__.' has been called');
}
public function classOneMethodTwo()
{
dd(__FUNCTION__.' has been called');
}
public function __call($methodName, $arguments)
{
$methodsTobeForwarededtoClassTwo = [
'classTwoMethodOne',
'classTwoMethodTwo',
// 'classTwoMethodThree'
//i have commented this method so you cannot access it
//from dynamic calls
];
if(in_array($methodName,$methodsTobeForwarededtoClassTwo))
{
return (new ClassTwo)->{$methodName}($arguments);
}
dd(sprintf(
'Call to undefined method ClassTwo::%s()', $methodName
));
}
}
So here comes the testing part.
$classOneobj = new ClassOne;
Basic Test
dump($classOneobj->classOneMethodOne()); will output as classOneMethodOne has been called
dump($classOneobj->classOneMethodTwo()); will output as classOneMethodTwo has been called
Dynamic Call Test
dump($classOneobj->classTwoMethodOne()); will output as classTwoMethodOne has been called
dump($classOneobj->classTwoMethodTwo()); will output as classOneMethodTwo has been called
dump($classOneobj->classTwoMethodThree()); will output as Call to undefined method ClassTwo::classTwoMethodThree() Because i have commented that method in __call function in ClassOne.
If you still need clarity please post a comment

partially mocking a class without affecting the private properties in PHP

I have a class with a lot of methods in which I need to mock only one method due to some sql incompatibility between mysql and in-memory sqlite database.
class OrderService implements OrderServiceContract
{
protected $deliveryService;
public function __construct(Delivery $deliveryService) // DI injected object
{
...
$this->deliveryService = $deliveryService;
...
}
public function methodNeedstoBeMocked()
{
....some sql related code...
}
public function returnToWarehouse($orderId)
{
DB::transaction(function() use ($orderId) {
...
$this->deliveryService->someOtherMethod($orderId); // problematic external service call
...
});
}
}
Now in my test I partially mock this class according to this doc link, and I call the returnToWarehouse from test but then it says that
Error : Call to a member function returnToWarehouse() on null.
meaning that the property $deliveryService doesn't exist on mock.
My test Implementation is as follows.
/**
* #test
*/
public function an_order_can_be_returned_to_warehouse()
{
...
...
$this->partialMock(OrderService::class, function ($mock) {
$mock->shouldReceive('methodNeedstoBeMocked')->andReturn(collect([]));
});
$orderService = app(OrderService::class);
$orderService->markOrderReturnedToWarehouse($order->id); // here is the problem gets triggered.
...
//assertions
}
What might be going wrong here? and what are some ways to mitigate this? Appreciate your help in advance.
The issue here is that partial test doubles from Mockery do not call the original constructor. For more information, please read the documentation here.
Alternatively, you could consider mocking the "problematic" method a bit differently. For example, you could extract that logic to a repository (since you mention that it is dealing with the database layer) that can then be mocked during your test.
Usually, When I have to mock some third party services I set up a Mock Like this.
This way you can set up easily your DI services
<?php
if (app()->environment('testing')) {
$this->app->bind(Delivery::class, static function () {
$service = \Mockery::mock(Delivery::class);
$service->shouldReceive('someOtherMethod')->andReturn([]);
return $service;
});
}

Laravel unit testing automatic dependency injection not working?

With Laravel Framework 5.8.36 I'm trying to run a test that calls a controller where the __construct method uses DI, like this:
class SomeController extends Controller {
public function __construct(XYZRepository $xyz_repository)
{
$this->xyz_repository = $xyz_repository;
}
public function doThisOtherThing(Request $request, $id)
{
try {
return response()->json($this->xyz_repository->doTheRepoThing($id), 200);
} catch (Exception $exception) {
return response($exception->getMessage(), 500);
}
}
}
This works fine if I run the code through the browser or call it like an api call in postman, but when I call the doThisOtherThing method from my test I get the following error:
ArgumentCountError: Too few arguments to function App\Http\Controllers\SomeController::__construct(), 0 passed in /var/www/tests/Unit/Controllers/SomeControllerTest.php on line 28 and exactly 1 expected
This is telling me that DI isn't working for some reason when I run tests. Any ideas? Here's my test:
public function testXYZShouldDoTheThing()
{
$some_controller = new SomeController();
$some_controller->doThisOtherThing(...args...);
...asserts...
}
I've tried things like using the bind and make methods on app in the setUp method but no success:
public function setUp(): void
{
parent::setUp();
$this->app->make('App\Repositories\XYZRepository');
}
That's correct. The whole idea of a unit test is that you mock the dependant services so you can control their in/output consistently.
You can create a mock version of your XYZRepository and inject it into your controller.
$xyzRepositoryMock = $this->createMock(XYZRepository::class);
$some_controller = new SomeController($xyzRepositoryMock);
$some_controller->doThisOtherThing(...args...);
This is not how Laravels service container works, when using the new keyword it never gets resolved through the container so Laravel cannot inject the required classes, you would have to pass them yourself in order to make it work like this.
What you can do is let the controller be resolved through the service container:
public function testXYZShouldDoTheThing()
{
$controller = $this->app->make(SomeController::class);
// Or use the global resolve helper
$controller = resolve(SomeController::class);
$some_controller->doThisOtherThing(...args...);
...asserts...
}
From the docs :
You may use the make method to resolve a class instance out of the
container. The make method accepts the name of the class or interface
you wish to resolve:
$api = $this->app->make('HelpSpot\API');
If you are in a location of your code that does not have access to the
$app variable, you may use the global resolve helper:
$api = resolve('HelpSpot\API');
PS:
I am not really a fan of testing controllers like you are trying to do here, I would rather create a feature test and test the route and verify everything works as expected.
Feature tests may test a larger portion of your code, including how
several objects interact with each other or even a full HTTP request
to a JSON endpoint.
something like this:
use Illuminate\Http\Response;
public function testXYZShouldDoTheThing()
{
$this->get('your/route')
->assertStatus(Response::HTTP_OK);
// assert response content is correct (assertJson etc.)
}

Mockery: Method does not exist on mock object

So I have an object which I am mocking and it has methods that are used for sending emails. I have gone from a static class over to a concrete class because I was having issues testing the static class with Mockery.
However I am now finding that when an eloquent model is saved it throws an event which fires off a listener. This listener is what is responsible for kicking off the call to the mock objects method.
I have found that when I do something like the following it works.
$model = factory(MyClass::class)->make();
$model->property = 'value';
$model->save()
$this->mailer->shouldHaveReceived('methodName')->with($arg1, $arg2, $arg3);
It will fail every time complaining that the method does not exist on the mock object, the worst part is if I go into my listener where the mock object method is called and I do something like this
echo '<pre>'.print_r(get_class_methods($this->mailer), true).'</pre>'; exit;
it shows that the method is in-fact in the array of methods returned from get_class_methods.
Now here is the kicker. Everything works perfectly and the test passes without any errors if I do the following:
$this->mailer->shouldReceive('methodName');
$model = factory(MyClass::class)->make();
$model->property = 'value';
$model->save()
Now I have been reading the Mockery Gotchas and it would appear that if my class was calling a method that truly didn't exist on the object via ___call then this error would be expected.
However my mailer class is NOT extending any other class that would have a __call method nor does it have that method defined on the class.
However as you may or may not know Eloquent Models do in-fact use the magic ___call method. However I am NOT mocking the model with Mockery, I am using factory models in Laravel but I am also not checking the model object for the method call, I am checking my concrete mailer class for the method call.
Anyone know why I might be getting this behavior from Mockery?
I am creating my mock object in Laravel 5.2 with the following code
public function mock($class)
{
$mock = Mockery::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
$this->mailer = $this->mock('Namespace\Classname');
How are you creating $this->mailer? In order to be able to use the spy functionality ->shouldHaveReceived, you need to have invoked Mockery::spy($className) or called the shouldIgnoreMissing method after creating the test double with Mockery::mock($className).

Resources