Issue with Mockery when injecting multiple mocked objects into controller - php

I'm using Mockery in my Laravel based PHP project to help test a Laravel MVC controller. Below is the relevant part of my controller class I'm trying to test.
class DevicesController extends Controller
{
private $deviceModel;
private $rfDeviceModel;
private $userModel;
private $userDeviceModel;
public function __construct(Device $deviceModel, RFDevice $rfDeviceModel, User $userModel, UserDevice $userDeviceModel)
{
$this->middleware('guest');
$this->deviceModel = $deviceModel;
$this->rfDeviceModel = $rfDeviceModel;
$this->userModel = $userModel;
$this->userDeviceModel = $userDeviceModel;
}
...
public function add(Request $request)
{
$name = $request->input('name');
$description = $request->input('description');
$onCode = $request->input('onCode');
$offCode = $request->input('offCode');
$pulseLength = $request->input('pulseLength');
$type = 1;
$currentUserId = $this->currentUser()->id;
$newDeviceId = $this->deviceModel->add($name, $description, $type)->id;
$this->rfDeviceModel->add($onCode, $offCode, $pulseLength, $newDeviceId);
$this->userDeviceModel->add($currentUserId, $newDeviceId);
return redirect()->route('devices');
}
}
In particular, I'm writing several unit tests around the controller's add(Request $request) function to make sure that each of the three model add(...) functions are called. My test case to handle this looks like the following:
public function testAdd_CallsAddForModels()
{
$mockDeviceModel = Mockery::mock(Device::class);
$mockDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(Device::class, $mockDeviceModel);
$mockRFDeviceModel = Mockery::mock(RFDevice::class);
$mockRFDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(RFDevice::class, $mockRFDeviceModel);
$mockUserDeviceModel = Mockery::mock(UserDevice::class);
$mockUserDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(UserDevice::class, $mockUserDeviceModel);
$user = $this->givenSingleUserExists();
$this->addDeviceForUser($user->user_id);
}
private function givenSingleUserExists()
{
$user = new User;
$name = self::$faker->name();
$email = self::$faker->email();
$userId = self::$faker->uuid();
$user = $user->add($name, $email, $userId);
return $user;
}
private function addDeviceForUser($userId)
{
$this->withSession([env('SESSION_USER_ID') => $userId])
->call('POST', '/devices/add', [
'name' => 'Taylor',
'description' => 'abcd',
'onCode' => 1,
'offCode' => 2,
'pulseLength' => 3
]);
}
When I run this test, I get the following output in the console:
There was 1 error:
1) Tests\Unit\Controller\DeviceControllerTest::testAdd_CallsAddForModels
Mockery\Exception\InvalidCountException: Method add() from Mockery_1_App_RFDevice should be called
exactly 1 times but called 0 times.
But the funny and perplexing thing is that if I comment out and combination of 2 of the 3 mockery sections, my test pass. This means to mean, that my code is actually working correctly, but for some reason in this case, I can't inject multiple mocked model objects into my controller and test them all at once. I guess I could split this up into three separate tests that make sure each model's add(...) function is called, but I want to do it all in one test case if possible. I also know I could use a repository pattern to wrap all the business logic in the controller's add(...) function into a single call, but then I would run into the same problem while testing the repository class.

You're not mocking the return values of the methods so this line attempts to access an attribute (id) on a null.
$newDeviceId = $this->deviceModel->add($name, $description, $type)->id;
You can fix this by adding a return value to your Device model mock like so:
$mockDeviceModel = Mockery::mock(Device::class);
$device = new Device;
$mockDeviceModel->shouldReceive('add')->withAnyArgs()->once()->andReturn($device);
To make such problems easier to debug in the future, change your error handler to re-throw the exceptions in a testing environment instead of rendering a valid HTML response.

Related

Design pattern that handles multiple steps

So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences

Symfony - findOneOrFail returns array message

In my Symfony service I wanted to add small edit so I decided it's better to do it inside the class.
In my controller I am getting storyId (it's not table ID, it's a string with different chars) from my Request like:
$story = json_decode($request->getContent(), true);
$storyId = $story['storyId'];
$freeStoryName = $this->storyRepo->findOneOrFail(['storyId' => $storyId]);
$story->freeStoryName($freeStoryName);
return $this->json(["message" => "SUCCESS"]);
And In my Entity class I handle it like:
public function freeStoryName(Story $story): Story
{
$this->setPreviousStoryName($story->getStoryName());
$story->setStoryName(null);
}
And I get the error message:
Call to a member function freeStoryName() on array
I know what the message means but do not get it? It's findOne() method..
And other question will be, do I need flush() method in the Entity class like I had in a service?
You are using freeStoryName on $story which is an array (json_decode($request->getContent(), true);)
You need to use your method with your result :
$story = json_decode($request->getContent(), true);
$storyId = $story['storyId'];
$freeStoryName = $this->storyRepo->findOneOrFail(['storyId' => $storyId]);
$freeStoryName->freeStoryName($freeStoryName);
return $this->json(["message" => "SUCCESS"]);
If you feel that it's a little weird to do it this way, you could change your method to:
public function freeStoryName()
{
$this->setPreviousStoryName($this->getStoryName());
$this->setStoryName(null);
}
And use it:
$freeStoryName->freeStoryName();

Mocking a service called by a controller from a WebTestCase

I have an API written using Symfony2 that I'm trying to write post hoc tests for. One of the endpoints uses an email service to send a password reset email to the user. I'd like to mock out this service so that I can check that the right information is sent to the service, and also prevent an email from actually being sent.
Here's the route I'm trying to test:
/**
* #Route("/me/password/resets")
* #Method({"POST"})
*/
public function requestResetAction(Request $request)
{
$userRepository = $this->get('app.repository.user_repository');
$userPasswordResetRepository = $this->get('app.repository.user_password_reset_repository');
$emailService = $this->get('app.service.email_service');
$authenticationLimitsService = $this->get('app.service.authentication_limits_service');
$now = new \DateTime();
$requestParams = $this->getRequestParams($request);
if (empty($requestParams->username)) {
throw new BadRequestHttpException("username parameter is missing");
}
$user = $userRepository->findOneByUsername($requestParams->username);
if ($user) {
if ($authenticationLimitsService->isUserBanned($user, $now)) {
throw new BadRequestHttpException("User temporarily banned because of repeated authentication failures");
}
$userPasswordResetRepository->deleteAllForUser($user);
$reset = $userPasswordResetRepository->createForUser($user);
$userPasswordResetRepository->saveUserPasswordReset($reset);
$authenticationLimitsService->logUserAction($user, UserAuthenticationLog::ACTION_PASSWORD_RESET, $now);
$emailService->sendPasswordResetEmail($user, $reset);
}
// We return 201 Created for every request so that we don't accidently
// leak the existence of usernames
return $this->jsonResponse("Created", $code=201);
}
I then have an ApiTestCase class that extends the Symfony WebTestCase to provide helper methods. This class contains a setup method that tries to mock the email service:
class ApiTestCase extends WebTestCase {
public function setup() {
$this->client = static::createClient(array(
'environment' => 'test'
));
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$this->mockEmailService = $mockEmailService;
}
And then in my actual test cases I'm trying to do something like this:
class CreatePasswordResetTest extends ApiTestCase {
public function testSendsEmail() {
$this->mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
}
So now the trick is to get the controller to use the mocked version of the email service. I have read about several different ways to achieve this, so far I've not had much luck.
Method 1: Use container->set()
See How to mock Symfony 2 service in a functional test?
In the setup() method tell the container what it should return when it's asked for the email service:
static::$kernel->getContainer()->set('app.service.email_service', $this->mockEmailService);
# or
$this->client->getContainer()->set('app.service.email_service', $this->mockEmailService);
This does not effect the controller at all. It still calls the original service. Some write ups I've seen mention that the mocked service is 'reset' after a single call. I'm not even seeing my first call mocked out so I'm not certain this issue is affecting me yet.
Is there another container I should be calling set on?
Or am I mocking out the service too late?
Method 2: AppTestKernel
See: http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
See: Symfony2 phpunit functional test custom user authentication fails after redirect (session related)
This one pulls me out of my depth when it comes to PHP and Symfony2 stuff (I'm not really a PHP dev).
The goal seems to be to change some kind of foundation class of the website to allow my mock service to be injected very early in the request.
I have a new AppTestKernel:
<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
And a new method in my ApiTestCase:
// https://stackoverflow.com/a/19705215
protected static function getKernelClass(){
$dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name('*TestKernel.php')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
}
$file = current($results);
$class = $file->getBasename('.php');
require_once $file;
return $class;
}
Then I alter my setup() to use the kernel modifier:
public function setup() {
...
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
static::$kernel->setKernelModifier(function($kernel) use ($mockEmailService) {
$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
});
$this->mockEmailService = $mockEmailService;
}
This works! However I now can't access the container in my other tests when I'm trying to do something like this:
$c = $this->client->getKernel()->getContainer();
$repo = $c->get('app.repository.user_password_reset_repository');
$resets = $repo->findByUser($user);
The getContainer() method returns null.
Should I be using the container differently?
Do I need to inject the container into the new kernel? It extends the original kernel so I don't really know why/how it's any different when it comes to the container stuff.
Method 3: Replace the service in config_test.yml
See: Symfony/PHPUnit mock services
This method requires that I write a new service class that overrides the email service. Writing a fixed mock class like this seems less useful than a regular dynamic mock. How can I test that certain methods have been called with certain parameters?
Method 4: Setup everything inside the test
Going on #Matteo's suggestion I wrote a test that did this:
public function testSendsEmail() {
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
static::$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
$this->client->getContainer()->set('app.service.email_service', $mockEmailService);
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
This test fails because the expected method sendPasswordResetEmail wasn't called:
There was 1 failure:
1) Tests\Integration\Api\MePassword\CreatePasswordResetTest::testSendsEmail
Expectation failed for method name is equal to <string:sendPasswordResetEmail> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
Thanks to Cered's advice I've managed to get something working that can test that the emails I expect to be sent actually are. I haven't been able to actually get the mocking to work so I'm a bit reluctant to mark this as "the" answer.
Here's a test that checks that an email is sent:
public function testSendsEmail() {
$this->client->enableProfiler();
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
$mailCollector = $this->client->getProfile()->getCollector('swiftmailer');
$this->assertEquals(1, $mailCollector->getMessageCount());
$collectedMessages = $mailCollector->getMessages();
$message = $collectedMessages[0];
$this->assertInstanceOf('Swift_Message', $message);
$this->assertEquals('Reset your password', $message->getSubject());
$this->assertEquals('info#example.com', key($message->getFrom()));
$this->assertEquals($this->user->getEmail(), key($message->getTo()));
$this->assertContains(
'This link is valid for 24 hours only.',
$message->getBody()
);
$resets = $this->getResets($this->user);
$this->assertContains(
$resets[0]->getToken(),
$message->getBody()
);
}
It works by enabling the Symfony profiler and inspecting the swiftmailer service. It's documented here: http://symfony.com/doc/current/email/testing.html

Symfony, Call service in PHP Unit

I have a unit test, where I need to call a service.
I did it that way:
<?php
namespace FM\PriceBundle\Tests\Service;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class PriceServiceTest extends KernelTestCase
{
private $container;
public function testFiulPrice()
{
self::bootKernel();
$this->container = self::$kernel->getContainer();
$productId = 1;
$id = 1;
$what = ['postal', 'departement', 'region', 'country', 'insee'];
$date = new \DateTime('2016-06-23');
$price = $this->container->get('fm.price.get')->fiulPrice($productId, $id, $what[0], $date);
$this->assertNotEmpty($price);
}
}
But I have this error:
There was 1 error:
1) FM\PriceBundle\Tests\Service\PriceServiceTest::testFiulPrice
Predis\Connection\ConnectionException: Connection refused [tcp://localhost:6379]
I guess you use redis in your app.
But I don't understand why do you want to implement test like this.
That test literally takes fixed data, an tries to get a result from external service that uses dependency (redis).
In my opinion, you need unit test for PriceService, another one for serivce registered as 'fm.price.get', and (maybe) the third one that use redis, but in this case you have to populate cache with some sample data.

How to test a class which depends on an Eloquent model with relationships?

What is the best way to write a unit test for a class which depends on an Eloquent model with relationships? E.g.
real object (with database). This is easy, but slow.
real object (no database). I can create a new object but I can't see how to set the related models without writing to the database.
mock object. I run into issues using Mockery with Eloquent models (e.g. see this question).
other solutions?
context: I'm using Laravel with Authority RBAC for access control. I want to find the best way to test my access rules in a unit test. Which means I need to pass the user dependencies to Authority during the test.
If you're writing unit tests, you shouldn't ever use a database. Testing against a database would be considered an integration test. Check out Roy Osherove's videos.
To answer your question, (and not having delved into Authority RBAC, I'd do something like this:
// assuming some RBAC class
SomeRBACClass extends RBACBaseClass {
function validate(UserClass $user) {
if (!$roles = $user->getRoles())
{
return false;
}
$allowed = array('admin', 'superadmin');
foreach ($roles as $role) {
if (in_array($role->name, $allowed)) {
return true;
}
}
return false;
}
}
SomeRBACClassTest extends TestCase {
function test_validate_WhenPassedUser_callsGetRolesOnUserWithNoArgs()
{
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
$user->shouldReceive('getRoles')->once()->withNoArgs();
$rbac->validate($user);
}
function test_validate_getRolesOnUserReturnsCollectionOfRoles_CallsGetAttributeWithNameOnFirstRole() {
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
// assuming $user->getRoles() returns a collection
$collection = new \Illuminate\Support\Collection(array(
$role1 = Mockery::mock('Role'),
$role2 = Mockery::mock('Role'),
));
$user->shouldReceive('getRoles')->andReturn($collection);
$role1->shouldReceive('getAttribute')->once()->with('name');
$rbac->validate($user);
}
function test_validate_getAttributeWithNameOnRoleReturnsValidRole_ReturnsTrue() {
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
// assuming $user->getRoles() returns a collection
$collection = new \Illuminate\Support\Collection(array(
$role1 = Mockery::mock('Role'),
$role2 = Mockery::mock('Role'),
));
$user->shouldReceive('getRoles')->andReturn($collection);
$role1->shouldReceive('getAttribute')->andReturn('admin');
$result = $rbac->validate($user);
$this->assertTrue($result);
}
This is not a thorough example of all the unit tests that I would write, but it's a start. E.g., I would also validate that when no roles are returned, that the result is false.

Categories