I'm trying to learn how to test properly and am struggling to get my head around mocks in the scenario below. I don't seem to be able to mock a class.
The main class uses a number of component classes to build a particular activity. I can test the component on it's own and mock it correctly but when I try to integrate test within the main class it calls the real service not the mock service.
This is in a Laravel 5.5 app.
I have a base class:
class booking {
private $calEventCreator
public function __construct(CalenderEventCreator $calEventCreator) {
$this->calEventCreator = $calEventCreator;
}
}
This is then extended by another class:
class EventType extends booking {
//do stuff
}
The CalenderEventCreator relies on an external service which I want to mock.
class CalendarEventCreator {
public function __construct(ExternalService $externalService) {
$this->externalService = $externalService;
}
}
In my test I have tried to do the following:
public function test_complete_golf_booking_is_created_no_ticket()
{
$this->booking = \App::make(\App\Booking\EventType::class);
$calendarMock = \Mockery::mock(ExternalService::class);
$calendarMock->shouldReceive([
'create' => 'return value 1',
])->once();
$this->booking->handle($this->attributes, 'booking');
}
But in trying to execute the test it's clear the ExyernalService is not using the mocked object.
I have tried re-arranging the code as follows:
$calendarMock = \Mockery::mock(Event::class);
$calendarMock->shouldReceive([
'create' => 'return value 1',
])->once();
$this->booking = \App::make(\App\Booking\EventType::class);
$this->booking->handle($this->attributes, 'booking');
}
and tried:
$this->booking = \App::make(\App\Booking\EventType::class, ['eventService'=>$calendarMock]);
But on each occassion the real service is called not the mock version
I'm learning this so apologies about fundamental errors but can someone explain how I should mock the external service correctly
Maybe there is a better way to achieve this but I'm using the following approach:
$calendarMock = \Mockery::mock(ExternalService::class);
$calendarMock->shouldReceive([
'create' => 'return value 1',
])->once();
$this->booking = new \App\Booking\EventType($calendarMock);
$this->booking->handle($this->attributes, 'booking');
Instead of using the service container to resolve the class, I call the constructor directly passing the mocked service.
UPDATE
Looking for other ways to do that, I found this answer. Using that solution, your code would look like this:
$calendarMock = \Mockery::mock(ExternalService::class);
$calendarMock->shouldReceive([
'create' => 'return value 1',
])->once();
$this->app->instance(ExternalService::class, $mock);
$this->booking = \App::make(\App\Booking\EventType::class);
$this->booking->handle($this->attributes, 'booking');
Related
Background
I have a system with a microservices setup. A few of these microservices run a laravel installation. In order to share some key models, a repo was shared using git/packagist.
Here is a diagram:
Microservice A
Microservice B
...
These both share Library C. This library has the shared models. This is outside of a normal laravel installation, but the composer includes "laravel/framework": "^9.0".
Note: There good external reasons to share the functionality - the microservices have come out of a monolith and are still developing fluidly and are not mature enough for a complete decoupling. This will come in time.
I wish to unit test these models.
Specifics
The requirement is that several models (User, Customer .. etc) all require addresses. Normalising these out would introduce complexity elsewhere that is not appropriate yet, so a trait is good for now. These have UK postcodes that require a specific validation against a database. Postcodes are modelled using a Postcode model.
I created a trait : AddressTrait. This offers some useful functionality. Included in this is a Postcode validation. This intercepts a set request in laravel (eg: $user->postcode = 'AB10 1AB)
/**
* Automatically updates the log/lat from the postcode
* #param $value
*/
public function setPostcodeAttribute($value): void
{
// update postcode
$this->attributes['postcode'] = strtoupper($value);
// now update lat/long
$postcode = Postcode::where('pcd', '=', str_replace(' ', '', $value))
->orWhere('pcd', '=', $value)
->first();
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
This works as expected.
Note - it is to be extended quite a bit further with much more complexity, but this is step 1 and completely represents the problem.
Testing
If I interact with the postcode attribute, such as $user->postcode = 'AB10 1AB, this attempts to load the Postcode from the database, and the following error occurs:
Error : Call to a member function connection() on null
^ This is expected.
I would like to unit test this: ie. no reaching out the class and mocking system/functional elements. Thus, I need to mock the Postcode load (Postcode::where(..) .. ).
As this is a static call, I have used mockery ("mockery/mockery": "dev-master").
Here is the current attempt:
// ...
use Mockery;
use PHPUnit\Framework\TestCase;
// ...
public function testPostcodeProcessing(): void
{
$postcode_value = 'AB10 1AB';
$postcode_content = [
'pcd' => $postcode_value,
'latitude' => '0.1',
'longitude' => '0.2'
];
$mock_postcode = Mockery::mock(Postcode::class);
$mock_postcode->shouldReceive('where')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('orWhere')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('first')->once()->andReturn($postcode_content);
$model = $this->createTraitImplementedClass();
$model->postcode = $postcode_value;
}
protected function createTraitImplementedClass(): Model
{
return new class extends Model {
use AddressTrait;
};
}
TLDR question
I would like to unit test this function: ie. no reaching out the class and mocking.
How do I mock a laravel/eloquent static call, given that:
this is to be tested outside laravel
there is no database connection
OR
How do I refactor this to allow it to be more testable
Super TLDR;
How do I mock the load in:
public function tldr(): void
{
// this eloquent lookup needs to be mocked (not moved, refactored etc etc..)
$postcode = Postcode::where('pcd', '=', 'AB10 1AB')->first();
}
Notes:
These are unit tests
I would prefer to do this "the laravel way", but given the unusual circumstances things such as mockery might make sense
May be a gotcha: I am using the phpunit PHPUnit\Framework\TestCase - not the usual PHP test case. This is not a "requirement", but I imagined a mock shouldn't need the extended features.
Any help with this would be appreciated!
What if you abstracted away the part where you get the postcode?
public function setPostcodeAttribute($value): void
{
// update postcode
$this->attributes['postcode'] = strtoupper($value);
// now update lat/long
$postcode = $this->getPostCode($value);
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
// you could make this method protected as well
// but if you do, your need to call the shouldAllowMockingProtectedMethods()
// when creating your mock
public function getPostCode(string $value): ?Postcode
{
return Postcode::where('pcd', '=', str_replace(' ', '', $value))
->orWhere('pcd', '=', $value)
->first();
}
If you do it like this, you no longer need to mock Eloquent Query builder at all. Partially mocking a class that uses that Address trait should give you what you need. I'm not sure if this works for anonymous classes though
public function test_existing_postcode()
{
// Arrange
$userMock = Mockery::mock(User::class)->makePartial();
$user = new User;
$postcode_value = 'AB10 1AB';
$postcode = new PostCode([
'pcd' => $postcode_value,
'latitude' => '0.1',
'longitude' => '0.2'
]);
// Expect
$userMock->expects()
->getPostCode($postcode_value)
->andReturn($postcode);
// Act
$user->postcode = $postcode_value;
// Assert
$this->assertEquals($user->latitude, $postcode->latitude);
$this->assertEquals($user->longitude, $postcode->longitude);
}
public function test_nonexisting_postcode()
{
// Arrange
$userMock = Mockery::mock(User::class)->makePartial();
$user = new User;
$postcode_value = 'AB10 1AB';
// Expect
$userMock->expects()
->getPostCode($postcode_value)
->andReturn(null);
// Act
$user->postcode = $postcode_value;
// Assert
$this->assertNull($user->latitude);
$this->assertNull($user->longitude);
}
Although I wouldn't recommend it, if you had a static method inside the Postcode model.
class Postcode extends Model
{
public static function getPostcodeByValue(string $value): ?Postcode
{
return Postcode::...
}
}
You could mock it with
$postcodeMock = \Mockery::mock('alias:Postcode');
$postcodeMock->shouldReceive('getPostcodeByValue')
->with($value)
->andReturn($postcode);
I'm not sure if expects() works, but if it does, you can also write this as
$postcodeMock = \Mockery::mock('alias:Postcode');
$postcodeMock->expects()
->getPostcodeByValue($value)
->andReturn($postcode);
Important: for this to work, the Postcode class should not have been loaded (by this or any previous tests). It's that fragile.
You can make your method more test friendly
Injectable external class to remove hidden dependencies
Keep the formatting/input validation outside if it is not related to "something" structural
Separate functionalities or the S in SOLID principles (move the lookup for Postcode instance to where it belongs)
like this
/**
* Automatically updates the log/lat from the postcode
* #param string $value
* #param Postcode $postcode
*/
public function setPostcodeAttribute($value, Postcode $postcode = null): void
{
// update postcode
$this->attributes['postcode'] = $value;
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
After some extensive looking into this, I've found the answer using mockery aliases. This is done as follows:
Isolate this class/test from the remainder of the tests
If you create an alias, this overwrites the class globally for the rest of the current process. It's risky, but this can be done and many of the problems sidestepped by running the test/class in a separate process.
This can be done using the docblock:
/**
* At a class level
* #runTestsInSeparateProcesses
* #preserveGlobalState disabled
*/
Mock the class as an alias
Aliases mock static classes. This is the key point I was missing during my question - I missed the alias: part.
public function testPostcodeProcessing(): void
{
// define this first to intercept the global instantiation
$mock_postcode = Mockery::mock('alias:' . Postcode::class);
// ...
}
The above mock will override ALL Postcode classes in this test/test class. Thus, it should be declared first.
Add your responses and assertions
This is entirely up to you, but here is the example and assertions I created.
/*
* Tests that the postcode processes correctly.
*/
public function testPostcodeProcessing(): void
{
// define this first to intercept the global instantiation
$mock_postcode = Mockery::mock('alias:' . Postcode::class);
// set up a returned class
$returned_postcode = new Postcode();
$postcode_pcd = 'AB10 1AB';
$postcode_latitude = 0.1;
$postcode_longitude = 0.2;
$returned_postcode->pcd = $postcode_pcd;
$returned_postcode->latitude = $postcode_latitude;
$returned_postcode->longitude = $postcode_longitude;
// Set up the mock
$mock_postcode->shouldReceive('where')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('orWhere')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('first')->once()->andReturn($returned_postcode);
$model = $this->createTraitImplementedClass();
$model->postcode = $postcode_pcd;
$this->assertEquals($postcode_pcd, $model->postcode, 'The postcode object pcd was not set');
$this->assertEquals($postcode_latitude, $model->latitude, 'The postcode object latitude was not loaded');
$this->assertEquals($postcode_longitude, $model->longitude, 'The postcode object longitude was not loaded');
}
Note - these are "step 1" tests. The real class is more complex, and the test will be more complex. However, this gives the core solution to the instantiation issue.
TLDR;
Run this in a separate process
Use an Alias (and remember to declare it as an alias - alias:SomeClass)
i want to select all users in the database that have the role ROLE_USER only but i get this problm when i call the function they say "Call to a member function getNbr() on null" i think bcoz i use Findby() , bcoz i use the same function in another call and it works great look at the code :
public function indexAction(Request $request)
{
$us = $this->getDoctrine()->getManager();
$locationus = $us->getRepository('AppBundle:Usr')->findBy(
[ 'roles' => ["ROLE_USER"] ]);
echo $nb_us = $locationus->getNbr();
if($authChecker->isGranted(['ROLE_ADMIN']))
{
return $this->render('settingAdmin/profiladmin.html.twig' , array(
'nb_us' => $nb_us,
));
}
and this is the other function in the UserRepository:
class UserRepository extends \Doctrine\ORM\EntityRepository
{
public function getNbr() {
return $this->createQueryBuilder('l')
->select('COUNT(l)')
->getQuery()
->getSingleScalarResult();
}
}
getNbr is method of UserRepository class, so it can be called only for this UserRepository class instance. This method returns total users count.
findBy returns array of entities (in you case all users with role ROLE_USER), not UserRepository class instance, so you can't use getNbr in context of this variable
If you want to get the length of array of entities (in you case all users with role ROLE_USER), just use count function:
echo $nb_us = count($locationus);
if($authChecker->isGranted(['ROLE_ADMIN']))
{
return $this->render('settingAdmin/profiladmin.html.twig' , array(
'nb_us' => $nb_us, 'locationus' => $locationus
));
}
There looks to be quite many things going on in the code there:
1) $us->getRepository('AppBundle:Usr') is probably typoed and should be $us->getRepository('AppBundle:User') instead (?) In general it would be safer to use $us->getRepository(AppBundle\User::class) so that syntax errors can be caught easier/earlier.
2) You are trying to invoke repository method on array with $locationus->getNbr() which is incorrect on multiple accounts (you cannot invoke functions on arrays - and repository methods cannot be invoked from entities either).
3) why is the code using echo?
4) as an additional note (assuming that this is roughly the full intended code), it would make sense to move all the getters & handling inside the if section so that the code will perform better (it doesn't do unnecessary database queries etc when the user doesn't have enough rights to access the view/information).
If I understood the intention correctly, in this case, the second repository function getNbr is superfluous here. If that is intending to just calculate the number of instances returned by the first find:
$locationus = $us->getRepository('AppBundle:User')->findBy(['roles' => ["ROLE_USER"] ]);
$nb_us = count($locationus);
Or alternatively (if you want to use and fix the getNbr repository function) then you don't need the first repository getter. This will require some rewriting of the repository function as well though:
$nb_us = $us->getRepository('AppBundle:User')->getNbr("ROLE_USER");
In a function in my controller I call this:
$item = Item::where('i_id', $Id)->where('type', 1)->first();
$firebaseData = app('firebase')->getDatabase()->getReference('items/'.$Id)->getSnapshot()->getValue();
Then I do a lot of "validation" between the data from the two sources above like:
if ($item->time_expires < strtotime(Carbon::now()) && $firebaseData['active'] == 1) {
return response()->json(['errors' => [trans('api.pleaserenew')]], 422);
}
And since this is not data coming from a user/request I cant use Laravels validate method
I dont want to keep this kind of logic inside my controller but where should I put it? Since part of my data is coming from Firebase I cant setup a Eloquent model to handle it either.
I recommend to receive the firebase data via a method within the model:
public function getFirebaseData()
{
app('firebase')->getDatabase()->getReference('items'/ . $this->i_id)->getSnapshot()->getValue();
}
That way you have the logic to receive the data decoupled from controller logic and moved it to where it makes more sense. Adding a validation method could work similarily within the model then:
public function validateData()
{
$combined = array_merge($this->toArray(), $this->getFirebaseData());
Validator::make($combined, [
'active' => 'in:1',
'time_expires' => 'before:' . Carbon::now(),
]);
}
The caveat with this is that the validation error will be thrown within the model instead of the controller, but that shouldn't really be an issue I don't think.
For any data you have in your application you can use Laravel validation.
You can merge your data and process it using Validator facade like this:
$combinedData = array_merge($item->toArray(), $firebaseData);
Validator::make($combinedData, [
'active' => 'required|in:1',
'time_expires' => 'required|before:' . Carbon::now()->toDateTimeString()
], $customMessageArray);
I think the best place for this code is some kind of service class you will inject to controller or other service class using Laravel dependency injection.
I'm using Apigility to create a rest application, where the back-end and front-end are pretty much independent applications.
Ok, on the Back-end I'm using 'zf-apigility-doctrine-query-provider' to create queries depending on the parameters sent via url (i.e localhost?instancia=10), but I need to process information using a MS SQL database stored function, something like this:
function createQuery(ResourceEvent $event, $entityClass, $parameters){
/* #var $queryBuilder \Doctrine\ORM\QueryBuilder */
$queryBuilder = parent::createQuery($event,$entityClass, $parameters);
if (!empty($parameters['instancia'])) {
$queryBuilder->andWhere($queryBuilder->expr()->eq('chapa.instancia', 'dbo.isItSpecial(:instancia)'))
->setParameter('instancia', $parameters['instancia']);
}
return $queryBuilder;
}
However it simply won't work, it won't accept the 'dbo.isItSpecial' and seems like I can't access the ServiceLocator, nor the EntityManager or anything but the Querybuilder.
I thought about creating a native query to get the result and the using it on the main query but seems like I can't create it.
Any ideas?
In what class is this method? Add some context to your question.
You do parent::createQuery which suggests that you are in a DoctrineResource instance. If this is true it means that both the ServiceLocator and the ObjectManager are simply available in the class.
You can read on doing native queries in Doctrine here in the Documentation:
$rsm = new ResultSetMapping();
$query = $entityManager->createNativeQuery(
'SELECT id, name, discr FROM users WHERE name = ?',
$rsm
);
$query->setParameter(1, 'romanb');
$users = $query->getResult();
Turns out I've found some ways to do this.
The class that the method was, extends this class
ZF\Apigility\Doctrine\Server\Query\Provider\DefaultOrm
That means I have access to the ObjectManager. The Documentation doesn't help much, but the ObjectManager is actually an EntityManager (ObjectManager is just the interface), to discover this I had to use the get_class PHP command.
With the entity manager I could have done what Wilt sugested, something like this:
$sqlNativa = $this->getObjectManager()->createNativeQuery("Select dbo.isItSpecial(:codInstancia) as codEleicao", $rsm);
However, I created a service that execute this function (it will be used in many places), so I also made a factory that set this service to the query provider class, on the configuration file it's something like this.
'zf-apigility-doctrine-query-provider' => array(
'factories' => array(
'instanciaDefaultQuery' => 'Api\Instancia\instanciaQueryFactory',
),
),
And the factory looks like something like this (the service is executing the NativeQuery like the response from Wilt):
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Api\EleitoChapaOrgao\EleitoChapaOrgaoQuery;
class InstanciaQueryFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceManager){
$instanciaService = $serviceManager->getServiceLocator()->get('Application\Service\Instancia');
$query = new InstanciaQuery($instanciaService);
return $query;
}
}
Finally just add the constructor to the QueryProvider and the service will be avaliable there:
class InstanciaQuery extends DefaultOrm
{
protected $instanciaService;
public function __construct(Instancia $instanciaService)
{
$this->instanciaService = $instanciaService;
}
public function createQuery(ResourceEvent $event, $entityClass, $parameters)
{ /* The rest of the code goes here*/
Here is the constructor of the class I am writing a test suite for (it extends mysqli):
function __construct(Config $c)
{
// store config file
$this->config = $c;
// do mysqli constructor
parent::__construct(
$this->config['db_host'],
$this->config['db_user'],
$this->config['db_pass'],
$this->config['db_dbname']
);
}
The Config class passed to the constructor implements the arrayaccess interface built in to php:
class Config implements arrayaccess{...}
How do I mock/stub the Config object? Which should I use and why?
Thanks in advance!
If you can easily create a Config instance from an array, that would be my preference. While you want to test your units in isolation where practical, simple collaborators such as Config should be safe enough to use in the test. The code to set it up will probably be easier to read and write (less error-prone) than the equivalent mock object.
$configValues = array(
'db_host' => '...',
'db_user' => '...',
'db_pass' => '...',
'db_dbname' => '...',
);
$config = new Config($configValues);
That being said, you mock an object implementing ArrayAccess just as you would any other object.
$config = $this->getMock('Config', array('offsetGet'));
$config->expects($this->any())
->method('offsetGet')
->will($this->returnCallback(
function ($key) use ($configValues) {
return $configValues[$key];
}
);
You can also use at to impose a specific order of access, but you'll make the test very brittle that way.
8 years after the question asked, 5 years after it was first answered I had the same question and came to a similar conclusion. This is what I did, which is basically the same as the second part of David's accepted answer, except I'm using a later version of PHPUnit.
Basically you can mock the ArrayAccess interface methods. Just need to remember that you probably want to mock both offsetGet and offsetExists (you should always check an array key exists before you use it otherwise you could encounter an E_NOTICE error and unpredictable behaviour in your code if it doesn't exist).
$thingyWithArrayAccess = $this->createMock(ThingyWithArrayAccess::class);
$thingyWithArrayAccess->method('offsetGet')
->with('your-offset-here')
->willReturn('test-value-1');
$thingyWithArrayAccess->method('offsetExists')
->with($'your-offset-here')
->willReturn(true);
Of course, you could have a real array in the test to work with, like
$theArray = [
'your-offset-here-1' => 'your-mock-value-for-offset-1',
];
$thingyWithArrayAccess = $this->createMock(ThingyWithArrayAccess::class);
$thingyWithArrayAccess->method('offsetGet')
->willReturnCallback(
function ($offset) use ($theArray) {
return $theArray[$offset];
}
);
$thingyWithArrayAccess->method('offsetExists')
->willReturnCallback(
function ($offset) use ($theArray) {
return array_key_exists($offset, $theArray);
}
);