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');
}
Related
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],
]
];
}
I've got a little problem on my hands. I can't manage to get the current controller name from the request in a Service Provider.
I want to dynamically provide a different repository by typehinting the same interface based on the controller I'm currently in.
My current service provider looks something like this, but the request()->route() returns null.
private $currentController;
private static $controllerBindings = [
'FooController' => ['RepositoryInterface' => 'FooRepository'],
];
private static $bindings = [
'SomeInterface' => 'SomeRepository'
];
public function boot()
{
$controller = request()->route()->getAction()['controller'];
$controller = preg_replace('/#[a-zA-Z0-9]+/', '', $controller);
$this->currentController = $controller;
}
public function register()
{
if ( array_key_exists($this->currentController, self::$controllerBindings) ) {
foreach (self::$controllerBindings[$this->currentController] as $interface => $dependency) {
app()->bind($interface, $dependency);
}
}
else {
foreach (self::$bindings as $interface => $dependency) {
app()->bind($interface, $dependency);
}
}
}
I've tried doing this and it gives: BindingResolutionException in Container.php line 748:
Target [App\Business\Interfaces\RepositoryInterface] is not instantiable while building [App\Http\Controllers\Admin\SettingsController].
private static $bindings = [
'App\Http\Controllers\SettingsController' => [
'App\Business\Interfaces\RepositoryInterface' => 'App\Business\Admin\Repositories\SettingsRepository',
],
];
public function boot()
{
}
public function register()
{
foreach (self::$bindings as $entity => $binding) {
if ( is_array($binding) ) {
$this->bindEntityFromArray($entity, $binding);
}
else {
app()->bind($entity, $binding);
}
}
}
private function bindEntityFromArray($entity, array $bindings)
{
foreach ($bindings as $interface => $dependency) {
app()->when($entity)->needs($interface)->give($dependency);
}
}
I inject the RepositoryInterface into the Controller's constructor.
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);
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([]);
i tried follow from http://book.cakephp.org/2.0/en/development/sessions.html#creating-a-custom-session-handler then i cant solve this and im really confuse about this :(
become like this:
<?php
App::uses('DatabaseSession', 'Model/Datasource/Session');
class CartSession implements CakeSessionHandlerInterface {
public $cacheKey;
public function __construct() {
$this->cacheKey = Configure::read('Session.handler.cache');
parent::__construct();
}
// read data from the session.
public function read($id) {
$result = Cache::read($id, $this->cacheKey);
if ($result) {
return $result;
}
return parent::read($id);
}
// write data into the session.
public function write($id, $data) {
$result = Cache::write($id, $data, $this->cacheKey);
if ($result) {
return parent::write($id, $data);
}
return false;
}
// destroy a session.
public function destroy($id) {
$result = Cache::delete($id, $this->cacheKey);
if ($result) {
return parent::destroy($id);
}
return false;
}
// removes expired sessions.
public function gc($expires = null) {
return Cache::gc($this->cacheKey) && parent::gc($expires);
}
}
?>
Output Error:
Fatal error: Class CartSession contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (CakeSessionHandlerInterface::open, CakeSessionHandlerInterface::close) in /Users/user/Sites/app/shop/Model/Datasource/Session/CartSession.php on line 43
I did added in core.php:
Configure::write('Session', array(
'defaults' => 'database',
'handler' => array(
'engine' => 'CartSession',
'model' => 'Session',
'cache' => 'apc'
)
));
Cache::config('apc', array('Engine' => 'Apc'));
You need to extend DatabaseSession. So your class declaration will look like:
class CartSession extends DatabaseSession implements CakeSessionHandlerInterface