Laravel Unit testing Eloquent insertion - php

I'm currently having some troubles in testing a function in Laravel. This function is a simple save user function.
The current structure involves a User
class User extends Authenticatable
Then I have a UserController
class UserController extends Controller
{
protected $user;
public function __construct(User $user)
{
$this->user = $user;
$this->middleware('admins');
}
The save function is defined on the UserController class, this class only assigns the request variables and uses Eloquent save function to save to database.
The function signature is the following:
public function storeUser($request)
{
$this->user->name = $request->name;
$this->user->email = $request->email;
$this->user->country_id = $request->country_id;
return $this->user->save();
}
The NewAccountRequest object extends from Request and has the validation rules for the request.
class NewAccountRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|min:6|max:60',
];
}
}
My problem is how can I unit test this storeUser function.
I have the current test:
public function testSaveUserWithEmptyRequest()
{
$user = $this->createMock(User::class);
$controller = new UserController($user);
$request = $this->createMock(NewAccountRequest::class);
$store = $controller->storeUser($request);
$this->assertFalse($store);
}
I'm mocking both User and NewAccountRequest, the problem is that the assertion should be false, from the Eloquent save. Instead I'm getting Null. Any idea on how can I correctly test the function?

<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseTransactions; // Laravel will automatically roll back changes that happens in every test
public function testSaveUserWithEmptyRequest()
{
$user = new User();
$controller = new UserController($user);
$request = $this->createMock(NewAccountRequest::class);
$store = $controller->storeUser($request);
$this->assertFalse($store);
}
}
This is exactly what you are trying to do, but unfortunately this will fail due to database exceptions...
Mocking a request or even manually crafting it will not do the data input validation.. and in your example password field is not nullable and will cause PDOException: SQLSTATE[HY000]: General error: 1364 Field 'password' doesn't have a default value
The recommended way to test functions depending on request, is to use http test helpers provided by laravel like $response = $this->post('/user', ['name' => 'Sally']);
A much better approach is to use the repository design pattern.. this simply means collate your database functions into separate classes and call it from controllers ..

Related

PHP: Mockery Mock variable $user = Auth::user()

So, I am trying to mock a service method.
In my service file:
/**
* Return all Api Keys for current user.
*
* #return Collection
*/
public function getApiKeys(): Collection
{
$user = Auth::user();
return ApiKey::where('org_id', $user->organizationId)->get();
}
How do I mock this?
<?php
namespace App\Services;
use PHPUnit\Framework\TestCase;
use Mockery as m;
class ApiKeysServiceTest extends TestCase
{
public function setUp()
{
parent::setUp();
/* Mock Dependencies */
}
public function tearDown()
{
m::close();
}
public function testGetApiKeys()
{
/* How to test? $user = Auth::user() */
$apiKeysService->getApiKeys();
}
}
In my TestCase class I have:
public function loginWithFakeUser()
{
$user = new GenericUser([
'id' => 1,
'organizationId' => '1234'
]);
$this->be($user);
}
What I want to do is test this method. Maybe this involves restructuring my code so that $user = Auth::user() is not called in the method. If this is the case, any thoughts as to where it should go?
Thanks for your feedback.
In your testGetApiKeys method you're not setting up the world. Make a mock user (using a factory as suggested in the comments factory('App\User')->create()), then setup an apiKey again using the factory, then call the method and assert it's what you've setup. An example with your code
public function loginWithFakeUser()
{
$user = factory('App\User')->create();
$this->be($user);
}
public function testApiSomething()
{
$this->loginWithFakeUser();
// do something to invoke the api...
// assert results
}
A good blueprint for the test structure is:
Given we have something (setup all the needed components)
If the user does some action (visits a page or whatever)
Then ensure the result of the action is what you expect (for example the status is 200)

Laravel - Initialize class only for specific routes

In my app I've got a group of routes which need some bootstraping before dispatching.
To illustrate the situation:
There is a special routes group with prefix 'app'. All of this routes have also some params:
site.dev/app/index?age=11&else=af3fs4ta21
Without these params user shouldn't be allowed to access route. I've got it done by creating a simple route middleware.
if (!$request->exists('age') || !$request->exists('else')) {
return redirect('/');
}
Next step is to initialize a class which takes route parameters as a construct arguments. Then param "else" is being used as a argument to db calls. I need to access this class in every route from /app route group.
In order to achive that I tried setting up a serviceprovider:
public function register()
{
$this->app->singleton(Dual::class, function ($app) {
return new Dual($this->app->request->all());
});
}
Then I created a special controller extending BaseController and passing Dual class to its constructor.
class DualController extends Controller
{
public function __construct(Request $request, Dual $dual)
{
$this->middleware(\App\Http\Middleware\DualMiddleware::class);
$this->dual = $dual;
}
}
And then every single controller is extending DualController and accessing Dual class by $this->dual->method().
It is working if route params are in their place and there is already a row in a database.
The problem
This middleware is executed AFTER ServiceProvider & DualController are initializing class Dual. So, middleware is not really working. If route params are not present it is going to fail.
Moreover, in case that there is no required row in database for some reason, Dual class will not be initialized (as it depends on calls to db) and whole app will crash saying that I am trying to perform operations on null.
Desired behaviour
First check route for params presence.
Second, check if there is row in db with key from route.
Third - try to initialize Dual class and pass it to all controllers used by route group /app.
If any of the steps fail -> display proper message.
Part of dual class:
class Dual
{
protected $client = null;
public $config = [];
public function __construct($config)
{
$this->config = $config;
$this->bootstrap();
}
public function getEli()
{
$eli = Eli::where(['else' => $this->config['else']])->first();
return $eli;
}
public function instantiateClient()
{
$client = Client::factory(Client::ADAPTER_OAUTH, [
'entrypoint' => $this->getEli()->eli_url,
'client_id' => '111',
'client_secret' => '111',
]);
$client->setAccessToken($this->getEli()->accessToken()->first()->access_token);
return $client;
}
public function getClient()
{
if ($this->client === null)
{
throw new \Exception('Client is NOT instantiated');
}
return $this->client;
}
public function bootstrap()
{
$this->client = $this->instantiateClient();
}
You can do this in middleware:
$isElseExists = Model::where('else', request('else'))->first();
if (request('age') && request('else') && $isElseExists) {
return $next($request);
} else {
return back()->with('error', 'You are not allowed');
}
If everything is fine, controller method will be executed. Then you'll be able to inject Dual class without any additional logic.
If something is wrong, a user will be redirected to previous URI with error message flashed into session.

Having some trouble understanding Facades and Service Providers

I've been reading the documentation up and down now, still not sure what I'm doing wrong. In my opinion the documentation is very difficult to understand for a beginner.
Anyway, I'm trying to make something akin to the Auth::user() method, where it returns additional data about a logged in user that I will be needing for this application.
I have this helper class here:
namespace App\Helpers;
use Auth;
use Illuminate\Http\Request;
use App\Models\Grouping\User;
use App\Models\Grouping\Client;
use App\Models\Grouping\Rank;
class ClientUser {
public function __construct($request) {
$this->request = $request;
}
public function client() {
return Client::find($this->request->session()->get('client_id'));
}
public function auth() {
if (Auth::check()) {
// Get the client
$client = $this->client();
// Get the client's user
$user = $client->users()->find(Auth::user()['id']);
// Get the rank of the logged in user
$rank = Rank::find($user->pivot->rank_id);
return [
'user' => $user,
'rank' => $rank,
'client' => $client
];
}
return null;
}
}
This is responsible for doing what I described, returning additional data that I can't get through Auth::user(). Now I'm trying to register this class in the AuthServiceProvider
public function register()
{
// Register client auth
$request = $this->app->request;
$this->app->singleton(ClientUser::class, function ($app) {
return new ClientUser($request);
});
}
Now what I don't understand is how I'm supposed to make this globally accessible throughout my app like Auth::user() is.
The problem with just making "importing" it is that it needs the request object, which is why I'm passing it through the service container.
Now here's where I'm stuck. I'm not able to access app in my controller or anywhere, and I can't define a Facade because a Facade expects you to return a string of the bound service that it should "alias?"
Change your service provider like this :
$this->app->bind('client.user', function ($app) {
return new ClientUser($app->request);
});
Create another class extended from Illuminate\Support\Facades\Facade.
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class ClientUserFacade extends Facade {
public static function getFacadeAccessor(){
return "client.user";
}
}
Add 'ClientUser => ClientUserFacade::class in alias key of app.php

Mocking class parameter that returns a mock

I am new to unit testing and trying to test a controller method in Laravel 5.1 and Mockery.
I am trying to test a registerEmail method I wrote, below:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Response;
use Mailchimp;
use Validator;
/**
* Class ApiController
* #package App\Http\Controllers
*/
class ApiController extends Controller
{
protected $mailchimpListId = null;
protected $mailchimp = null;
public function __construct(Mailchimp $mailchimp)
{
$this->mailchimp = $mailchimp;
$this->mailchimpListId = env('MAILCHIMP_LIST_ID');
}
/**
* #param Request $request
* #return \Illuminate\Http\JsonResponse
*/
public function registerEmail(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
]);
$email = $request->get('email');
try {
$subscribed = $this->mailchimp->lists->subscribe($this->mailchimpListId, [ 'email' => $email ]);
//var_dump($subscribed);
} catch (\Mailchimp_List_AlreadySubscribed $e) {
return Response::json([ 'mailchimpListAlreadySubscribed' => $e->getMessage() ], 422);
} catch (\Mailchimp_Error $e) {
return Response::json([ 'mailchimpError' => $e->getMessage() ], 422);
}
return Response::json([ 'success' => true ]);
}
}
I am attempting to mock the Mailchimp object to work in this situation.
So far, my test looks as follows:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class HomeRouteTest extends TestCase
{
use WithoutMiddleware;
public function testMailchimpReturnsDuplicate() {
$listMock = Mockery::mock('Mailchimp_Lists')
->shouldReceive('subscribe')
->once()
->andThrow(\Mailchimp_List_AlreadySubscribed::class);
$mailchimp = Mockery::mock('Mailchimp')->lists = $listMock;
$this->post('/api/register-email', ['email'=>'duplicate#email.com'])->assertJson(
'{"mailchimpListAlreadySubscribed": "duplicate#email.com is already subscribed to the list."}'
);
}
}
I have phpUnit returning a failed test.
HomeRouteTest::testMailchimpReturnsDuplicate
Mockery\Exception\InvalidCountException: Method subscribe() from Mockery_0_Mailchimp_Lists should be called exactly 1 times but called 0 times.
Also, if I assert the status code is 422, phpUnit reports it is receiving a status code 200.
It works fine when I test it manually, but I imagine I am overlooking something fairly easy.
I managed to solve it myself. I eventually moved the subscribe into a seperate Job class, and was able to test that be redefining the Mailchimp class in the test file.
class Mailchimp {
public $lists;
public function __construct($lists) {
$this->lists = $lists;
}
}
class Mailchimp_List_AlreadySubscribed extends Exception {}
And one test
public function testSubscribeToMailchimp() {
// create job
$subscriber = factory(App\Models\Subscriber::class)->create();
$job = new App\Jobs\SubscribeToList($subscriber);
// set up Mailchimp mock
$lists = Mockery::mock()
->shouldReceive('subscribe')
->once()
->andReturn(true)
->getMock();
$mailchimp = new Mailchimp($lists);
// handle job
$job->handle($mailchimp);
// subscriber should be marked subscribed
$this->assertTrue($subscriber->subscribed);
}
Mockery will expect the class being passed in to the controller be a mock object as you can see here in their docs:
class Temperature
{
public function __construct($service)
{
$this->_service = $service;
}
}
Unit Test
$service = m::mock('service');
$service->shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14);
$temperature = new Temperature($service);
In laravel IoC it autoloads the classes and injects them, but since its not autoloading Mailchimp_Lists class it won't be a mock object. Mailchimp is requiring the class atop it's main class require_once 'Mailchimp/Lists.php';
Then Mailchimp is then loading the class automatically in the constructor
$this->lists = new Mailchimp_Lists($this);
I don't think you'll be able to mock that class very easily out of the box. Since there isn't away to pass in the mock object to Mailchimp class and have it replace the instance of the real Mailchimp_Lists
I see you are trying to overwrite the lists member variable with a new Mock before you call the controller. Are you certain that the lists object is being replaced with you mocked one? Try seeing what the classes are in the controller when it gets loaded and see if it is in fact getting overridden.

Laravel mock with Mockery Eloquent models

I'm developing a PHP (5.4.25) application with laravel(4.2) framework. I'd like test my UserController with Mockery, so I've fit my UserController in this way:
class UsersController extends \BaseController {
protected $user;
public function __construct(User $user) {
$this->user = $user;
$this->beforeFilter('csrf', array('on'=>'post'));
}
public function store() {
$validator = Validator::make(Input::all(), User::$rules);
if ( $validator->passes() ) {
$this->user->username = Input::get('username');
$this->user->password = Hash::make(Input::get('password'));
$this->user->first_name = Input::get('first_name');
$this->user->last_name = Input::get('last_name');
$this->user->email = Input::get('email');
$this->user->save();
return true;
} else {
return false;
}
}
I want mock Eloquent User model so i develop my UsersControllerTest so:
class UsersControllerTest extends TestCase {
private $mock;
public function __construct() {}
public function setUp() {
parent::setUp();
$this->createApplication();
}
public function tearDown() {
parent::tearDown();
Mockery::close();
}
public function testStore() {
$this->mock = Mockery::mock('Eloquent','User[save]');
$this->mock
->shouldReceive('save')
->once()
->andReturn('true');
$this->app->instance('User', $this->mock);
$data['username'] = 'qwerety';
$data['first_name'] = 'asd';
$data['last_name'] = 'asd123';
$data['email'] = 'test#gmail.com';
$data['password'] = 'password';
$data['password_confirmation'] = 'password';
$response = $this->call('POST', 'users', $data);
var_dump($response->getContent());
}
}
When I run my test it returns me this error:
Mockery\Exception\InvalidCountException : Method save() from Mockery_0_User should be called
exactly 1 times but called 0 times.
Why? What's wrong?
EDIT: I found the problem - If I don't use mock object all works fine and the controller create a new user in the DB, but when I use mock the Input:all() method returns empty array.
--
Thanks
i had this same issue when i started testing.....
the thing there is that, in your userscontroller store method you are actually saving the user to the database and base on your code it might work just fine but you are surprise that it is actually failing the test.
Now think of it this way, you mock the user, and you told your test that when ever i call User model, please give me the mock version of my user, like you did in your code line below
$this->app->instance('User', $this->mock);
Now the problem is that you need to also call the mock version of save() method from the through the mock version of User like so:
$this->mock->save();
Consider the following Example:
public function testStore()
{
$input = ['name', 'Canaan'];
$this->mock
->shouldReceive('create')
->once()->with($input);
$this->app->instance('User', $this->mock);
$this->mock->create($input);
$this->call('POST', 'users', $input);
}
i hope it helps you.
The way you are testing this, in the controller constructor is passed an instance of the real Eloquent model, not the mock. If you want to test the facades (as clearly stated in the documentation) you should call the shouldReceive() method directly on the facade to have it mocked.
But, since you are redefining the $this->user variable in the store() method of the controller, it will not be called, unless you remove the hardcoded instantiation and use the injected $user.
Edit: i overlooke the $this->app->instance('User', $this->mock); line. So, the problem may be due the fact that in the store method you are getting a new class instance directly, and not via the Laravel IoC container. instead of $this->user = new User; in your store method, you should get it via App::make('User');
The problem was in $this->createApplication();.
I have commented that line and Input::all() works fine with all input parameters!

Categories