Mocking problems with PhpSpec and Prophecy - php

I am trying to test a method that I have using PhpSpec and Prophecy. I am having a little trouble getting it to work, though. I don't want to use actual values for the mock, so I have used Argument::any(), but it seems to be wanting to execute the actual call inside the method. I thought that this was what mocks were to prevent against?
My Class:
class CreatePostValidator
{
protected $validator;
protected $data;
protected $rules = [
'title' => 'required',
'body' => 'required',
'author' => 'required',
'slug' => 'required'
];
public function __construct(Factory $validator)
{
$this->validator = $validator;
}
public function with($data)
{
$this->data = $data;
}
public function passes()
{
$validator = $this->validator->make($this->data, $this->rules);
if ($validator->fails())
{
return false;
}
return true;
}
}
My Test:
class CreatePostValidatorSpec extends ObjectBehavior
{
function let(Factory $factory)
{
$this->beConstructedWith($factory);
}
function it_is_initializable()
{
$this->shouldHaveType('Blog\Service\Laravel\CreatePostValidator');
}
function it_should_return_false_if_validator_fails(
Factory $factory, Validator $validator)
{
$factory->make(Argument::any(), Argument::any())->willReturn($validator);
$validator->fails()->willReturn(true);
$this->with(Argument::any());
$this->passes()->shouldReturn(false);
}
}
And the error I am getting back:
error: Argument 1 passed to
Double\Illuminate\Validation\Factory\P2::make() must be of the type
array, object given, called in
/Users/will/development/personal/blog/app/Blog/Service/Laravel/CreatePostValidator.php
on line 32 and defined in
/Users/will/development/personal/blog/vendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.php(49)
:
eval()'d code line 10

This one is not an expectation, so just pass [] there:
$this->with([]);

Related

Symfony EventSubscriber using KernelEvents throw me Offset 0 error

So I'm learning the basis of creating an API.
I'm trying to interact on the creation of a new user by using the API, however I need with symfony to hash passwords.
I made a PasswordEncoderSubscriber method which hash the password before inserting it into the database.
private $encoder;
public function __construct(PasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['encodePassword' => EventPriorities::PRE_WRITE]
];
}
public function encodePassword(ViewEvent $event)
{
$result = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if ($result instanceof User && $method === "POST") {
$hash = $this->encoder->encodePassword($result, $result->getPassword());
$result->setPassword($hash);
}
}
I'm using the KernelEvents::View to call the function encodePassword before it written in the database with EventPriorities::PRE_WRITE.
Here's the error I got : Notice: Undefined offset: 0.
The code breaks just after the KernelEvents::VIEW did I forget something ?
Thanks!
According to symfony manual you should provide handler and it's priority as array with 2 items:
return [
KernelEvents::EXCEPTION => [
['processException', 10],
],
];
So, your code should be:
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['encodePassword', EventPriorities::PRE_WRITE],
]
];
}

How to create a callback in laravel validation when it failed-validated and passed-validated?

I have here a validation in my custom request file.
class AuthRequest extends FormRequest
{
public function store()
{
return $this->validate([
'first_name' => ['required','min:2','max:30',new PersonNameRule],
'last_name' => ['required','min:2','max:30',new PersonNameRule],
'username' => ['required','confirmed',new UsernameRule]
]);
}
public function rules(){ return []; }
}
In my controller, this is how use it.
public function store(AuthRequest $request)
{
$data = $request->store();
return request()->all();
}
My question is how can I do these things below:
when validation failed - create a session / session(['attempt' => session('attempt')+1 ?? 1]);
when validation passed - destroy the session / session()->forget('attempt')
#mrhn is right you did not fill the rules inside the function, so the FormRequest will always return false. What you did instead, you prefer to create your own method(s) and by using the $this->validate().
Now here's how to achieve your problem, in file ..\Illuminate\Validation\Validator.php find the validate() function, and put those session you desired to perform, like these below.
public function validate()
{
if ($this->fails()) {
session(['attempt' => session('attempt')+1 ?? 1]);
throw new ValidationException($this);
}else{
session()->forget('attempt');
}
return $this->validated();
}
The solution above is global which means it will perform everytime you use $this->validate().
You can use Validator instance instead of calling validate()
$validator = Validator::make($request->all(), [
'first_name' => ['required','min:2','max:30',new PersonNameRule],
'last_name' => ['required','min:2','max:30',new PersonNameRule],
'username' => ['required','confirmed',new UsernameRule]
]);
if ($validator->fails()) {
// create a session
} else {
// destroy the session
}
You can see more in the doc here: https://laravel.com/docs/7.x/validation#manually-creating-validators
Firstly i will convert your validation to a form request, this will automatically resolve when injected into a controller.
UserCreateRequest extends FormRequest {
public function rules() {
'first_name' => ['required','min:2','max:30',new PersonNameRule],
'last_name' => ['required','min:2','max:30',new PersonNameRule],
'username' => ['required','confirmed',new UsernameRule]
}
}
To use it inject it like so.
public create(UserCreateRequest $request) {
...
}
Here you can utilize two callback methods passedValidation() and failedValidation(), in your form request.
protected function failedValidation(Validator $validator) {
session(['attempt' => session('attempt')+1 ?? 1]);
return parent::failedValidation($validator);
}
protected function passedValidation() {
session()->forget('attempt')
return parent::passedValidation();
}

Laravel 4 - redirect from a repository when not returning the called method

I am using a repository pattern in my Laravel 4 project but come across something which I think I am doing incorrectly.
I am doing user validation, before saving a new user.
I have one method in my controller for this:
public function addNewUser() {
$validation = $this->userCreator->validateUser($input);
if ( $validation['success'] === false )
{
return Redirect::back()
->withErrors($validation['errors'])
->withInput($input);
}
return $this->userCreator->saveUser($input);
}
Then the validateUser method is:
public function validate($input) {
$rules = array(
'first_name' => 'required',
'last_name' => 'required',
'email_address' => 'unique:users'
);
$messages = [
];
$validation = Validator::make($input, $rules, $messages);
if ($validation->fails())
{
$failed = $validation->messages();
$response = ['success' => false, 'errors' => $failed];
return $response;
}
$response = ['success' => true];
return $response;
}
This may be okay, but I dont like doing the if statement in my controller? I would rather be able to handle that in my validation class.
But to be able to redirect from the validation class, I need to return the method in the controller.
What if I then want to have 5 methods called, I cant return them all?
I would like to be able to simply call the methods in order, then in their respective class handle what I need to and if there is any errors redirect or deal with them. But if everything is okay, simply ignore it and move to the next function.
So example:
public function addNewUser()
{
$this->userCreator->validateUser($input);
$this->userCreator->formatInput($input);
$this->userCreator->sendEmails($input);
return $this->userCreator->saveUser($input);
}
If doing the if statement in the controller isn't as bad as I think then I can continue, but this seems incorrect?
For repository pattern, you can use this :-
setup your basemodel like this
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class BaseModel extends Model{
protected static $rules=null;
protected $errors=null;
public function validateForCreation($data)
{
$validation=\Validator::make($data,static::$rules);
if($validation->fails())
{
$this->errors=$validation->messages();
return false;
}
return true;
}
/**
* #return errors
*/
public function getErrors() { return $this->errors; }
}
now in your repository, add these methods
protected $model;
protected $errors=null;
public function model(){ return $this->model; }
public function getErrors(){ return $this->errors; }
public function create($inputs)
{
if(!$this->model->validateForCreation($inputs))
{
$this->errors=$this->model->getErrors();
return false;
}
$new=$this->model->create($inputs);
return $new;
}
and the controller will look like this..
public function postCreate(Request $request)
{
$inputs=$request->all();
if($new=$this->repo->create($inputs))
{
return redirect()->back()
->with('flash_message','Created Successfully');
}
return redirect()->back()->withInput()->withErrors($this->repo->getErrors())
->with('flash_message','Whoops! there is some problem with your input.');
}

Mocking validator in Laravel with Mockery returning call to member function on a non-object

I'm trying to implement some unit tests for my REST controller. Everything works fine until I use the Validator facade. The index and show tests are working fine.
The error I'm getting is:
Fatal Error: Call to a member function setAttributeName() on a non-object in D:\....\controllers\AllergyController.
My code is:
//Unit test
class AllergyControllerTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->allergy = $this->mock('App\Modules\Patient\Repositories\IAllergyRepository');
}
public function mock($class)
{
$mock = Mockery::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
public function tearDown()
{
parent::tearDown();
Mockery::close();
}
public function testIndex()
{
$this->allergy->shouldReceive('all')->once();
$this->call('GET', 'api/allergy');
$this->assertResponseOk();
}
public function testShow()
{
$this->allergy->shouldReceive('find')->once()->andReturn(array());
$this->call('GET', 'api/allergy/1');
$this->assertResponseOk();
}
public function testStore()
{
$validator = Mockery::mock('stdClass');
Validator::swap($validator);
$input = array('name' => 'foo');
$this->allergy->shouldReceive('create')->once();
$validator->shouldReceive('make')->once();
$validator->shouldReceive('setAttributeNames')->once();
$this->call('POST', 'api/allergy', $input);
$this->assertResponseOk();
}
}
My controller:
class AllergyController extends \App\Controllers\BaseController
{
public function __construct(IAllergyRepository $allergy){
$this->allergy = $allergy;
}
public function index()
{
...
}
public function show($id)
{
...
}
public function store()
{
//define validation rules
$rules = array(
'name' => Config::get('Patient::validation.allergy.add.name')
);
//execute validation rules
$validator = Validator::make(Input::all(), $rules);
$validator->setAttributeNames(Config::get('Patient::validation.allergy.messages'));
if ($validator->fails()) {
return Response::json(array('status' => false, 'data' => $validator->messages()));
} else {
$allergy = $this->allergy->create(Input::all());
if ($allergy) {
return Response::json(array('status' => true, 'data' => $allergy));
} else {
$messages = new \Illuminate\Support\MessageBag;
$messages->add('error', 'Create failed! Please contact the site administrator or try again!');
return Response::json(array('status' => false, 'data' => $messages));
}
}
}
}
I can't seem to fgure out why it's throwing this error. When I call the controller with a normal api call it works fine.
Any help is much appreciated!
You are probably wanting to return the validator double from the stubbed make call.
$validator->shouldReceive('make')->once()->andReturn($validator);

Testing laravel repository which has model as a dependency

Problem is that i can't test one function, because it is touching other functions of the same repository.
Do I need to test one function in isolation from other functions in same repository, or it is normal that one function can access other functions in same repository ?
If function needs to be tested in isolation from other, how it can be done, because I don't understand how I can mock repository in which I'm working. I understand how to mock dependencies, but how to mock other functions in same repository ?
Am I mocking model correctly in setUp method in the test?
Code:
Real world binding of and repository:
// Bind User repository interface
$app->bind('MyApp\Repositories\User\UserInterface', function () {
return new EloquentUser(new User);
});
EloquentUser.php:
public function __construct(Model $user)
{
$this->user = $user;
}
public function findById($id)
{
return $this->user->find($id);
}
public function replace($data)
{
$user = $this->findById($data['user']['id']);
// If user not exists, create new one with defined values.
if ( ! $user) {
return $this->create($data);
} else {
return $this->update($data);
}
}
public function create($data)
{
$user = $this->user->create($data['user']);
if ($user) {
return $this->createProfile($user, $data['profile']);
} else {
return false;
}
}
private function createProfile($user, $profile)
{
return $user->profile()->create($profile);
}
public function update($user, $data)
{
foreach ($data['user'] as $key => $value) {
$user->{$key} = $value;
}
if (isset($data['profile']) && count($data['profile']) > 0) {
foreach ($data['profile'] as $key => $value) {
$user->profile->$key = $value;
}
}
return ($user->push()) ? $user : false;
}
EloquentUserTest.php
public function setUp()
{
parent::setUp();
$this->user = Mockery::mock('Illuminate\Database\Eloquent\Model', 'MyApp\Models\User\User');
App::instance('MyApp\Models\User\User', $this->user);
$this->repository = new EloquentUser($this->user);
}
public function testReplaceCallsCreateMethod()
{
$data = [
'user' => [
'id' => 1,
'email' => 'test#test.com',
],
'profile' => [
'name' => 'John Doe',
'image' => 'abcdef.png',
],
];
// Mock the "find" call that is made in findById()
$this->user->shouldReceive('find')->once()->andReturn(false);
// Mock the "create" call that is made in create() method
$this->user->shouldReceive('create')->once()->andReturn(true);
// Run replace method that i want to test
$result = $this->repository->replace($data);
$this->assertInstanceOf('Illuminate\Database\Eloquent\Model', $result, 'Should be an instance of Illuminate\Database\Eloquent\Model');
}
When running this test I got:
Fatal error: Call to a member function profile() on a non-object in C:\Htdocs\at.univemba.com\uv2\app\logic\Univemba\Repositories\User\EloquentUser.php on line 107
So it means that Test is trying to touch function in EloquentUser.php:
private function createProfile($user, $profile)
{
return $user->profile()->create($profile);
}
Do I need to mock createProfile ? because profile() cant be found. And if I need to do this, how can i do it because this function is in same repository that i'm testing?
Question is solved.
Just needed to create one more Model instance and pass it in mocked method.
My Working setUp method:
public function setUp()
{
parent::setUp();
$this->user = Mockery::mock('MyApp\Models\User\User');
App::instance('MyApp\Models\User\User', $this->user);
$this->repository = new EloquentUser($this->user);
}
Working test method:
public function testReplaceCallsCreateMethod()
{
$data = [
'user' => [
'id' => 1,
'email' => 'test#test.com',
'password' => 'plain',
],
'profile' => [
'name' => 'John Doe',
'image' => 'abcdef.png',
],
];
// Mock Model's find method
$this->user->shouldReceive('find')->once()->andReturn(false);
// Create new Model instance
$mockedUser = Mockery::mock('MyApp\Models\User\User');
// Mock Models profile->create and pass Model as a result of a function
$mockedUser->shouldReceive('profile->create')->with($data['profile'])->andReturn($mockedUser);
// Pass second instance Model as a result
$this->user->shouldReceive('create')->once()->andReturn($mockedUser);
// Now all $user->profile is properly mocked and will return correct data
$result = $this->repository->replace($data);
$this->assertInstanceOf('Illuminate\Database\Eloquent\Model', $result, 'Should be an instance of Illuminate\Database\Eloquent\Model');
}

Categories