You have asked the golden goose question. Basically facades are become less encouraged because of what you are describing. In Laravel 5 facades are not as common place as in 4
Confusion with Facades and testing
I'm trying to refactor my application, to learn about testing. I've already adopted the repository pattern, and managed to clear out my controllers. However, Facades confuse me.
I understand they just provide a way to attach into a service provider, and I also have my own Responder facade which has a couple of methods and returns a specific JSON structured response. However, it now seems I cannot use this, if I want to test my controller in isolation (so I will have to write tests for the Responder class too). Instead I inject the class into my constructor to allow for mocking.
This keeps making me ask the question: Why are Facades encouraged in the framework when in theory, they are bad for testing.
I understand you can mock some Laravel Facades with shouldReceive, but when I have my own Facades, what happens then?
So basically if your code is riddled with Facades, then it is 'untestable'. I'm just really surprised that so many tutorials (including ones on this site) don't even give a hint that using a Facade is the 'poor design approach', unless it's a tutorial specifically on testing.
I literally just refactored my code, so a very common task is now using a Facade... but now I've just learnt there is no way to test my controller methods with this in there, so I've just gone back to injecting this into my constructor... hence my thinking that for good coding standards (which we should all be encouraging), Facades are totally useless.
EDIT: Unless someone wants to prove me wrong, I aint no pro!
Controllers should be Acceptance or Functional tested rather than Unit Tested. Look into Codeception for Acceptance and Functional testing.
The way you could test this is Unit Test your Repository pattern and mock the classes that are injected. On the Controller, acceptance test the views and how the application is being rendered all together.
Controllers are gathering all of the code together and rendering the view therefore it is more likely you will want to test functionality like form submits and if the views are loading or what is on the page.
If you are testing other things besides this in your Controllers you may want to consider where that code is placed and why.
Also, Jeffrey has stated this in many videos! Good luck. Hope this helps.
Thanks @ross.edman!
In this case, my application is just an API, so I'm not rendering views, just JSON responses.
So am I right in saying:
Unit Test my controllers, basically mocking the repository implementations ensuring each controller method calls the correct repository methods?
Unit Test my custom classes, mocking any dependancies each class method has to make sure they're called.
When would functional tests come into play?
@Alias You can actually test JSON REST APIs with Codeception! You could Acceptance Test or Functional Test these and make mock POST / GET / PUT / DELETE calls on your endpoints in your controller.
Check this demo app out, they have a specific test suite just for the API. Also reference the REST module that Codeception has. You can use this in conjunction with the Laravel module too. In the demo app they are testing a RESTful Posts API and here are the tests for it.
I would unit test my custom classes to make sure each method is performing as it should. Then REST / API test the endpoints that are the controllers to mimic how a user or app would get data from your API.
They generated a new suite with these Modules to do this:
class_name: ApiTester
modules:
enabled: [Laravel4, REST, ApiHelper, Asserts]
@ross.edman Awesome, this is exactly what I was looking for. What are (these)[https://github.com/Codeception/sample-l4-app/blob/master/tests/api/PostsResourceCest.php] types of tests called then?
Now I just need to figure out how I can test my database (it's massive), without truncating and running the migrations every time... since it has a lot of records being generated. Unless I just create a dud-database with it all in, and test against that every time.
@Alias These would be functional because it is testing the database as well as how it used with everything combined.
Remember when Laravel is running tests the environment switches to testing. What I normally do is create a small database sample and add it to the _data folder in codeception. This should be just a small sample of how your data is configured. Notice in the root directory of the test app there is a codeception.yml file that sets the database file?
modules:
config:
Db:
dsn: ''
user: ''
password: ''
dump: tests/_data/dump.sql
Then you structure your tests around this small data sample. For consistency, you can setup Seeds that run specifically in your testing environment. That way you are not running a huge database that is being loaded and torn down everytime.
Here is how to set up the seeds for environments. You would need to run the migrations and then export the database into Codeception before running the tests I believe. Keeping the seeds in version control will help keep your database straight though when you make big changes:
if( App::environment() === 'testing' ) {
$this->call( 'TestingUsersTableSeeder' );
}
For an API test I would have a dataset I would just have a few records under each endpoint that you can test for GET calls and DELETE calls but the rest you will be adding or updating records so you don't need a lot.
You also may need to add the Db module to your suite to test the Database as well...
class_name: ApiTester
modules:
enabled: [Laravel4, REST, ApiHelper, Asserts, Db]
@ross.edman thanks for all your help, appreciated!
I seem to be having some issues while using Homestead though. My app is currently on 'myapp.app:8000', and the base route (/) just returns "test ok". This is so I know the web server is running.
So, in my API test I run:
$I = new ApiTester($scenario);
$I->wantTo('check the web server is runnung');
$I->sendGET('/');
$I->seeResponseCodeIs(200);
$I->seeResponseContains('test ok');
My yml file looks like:
class_name: ApiTester
modules:
enabled: [Laravel4, REST, ApiHelper, Asserts]
config:
REST:
url: http://myapp-api.app:8000
Basically running my API tests, I get back:
Scenario:
* I send get "/"
[Request] GET http://myapp-api.app:8000/
[Response]
[Headers] {"cache-control":["no-cache"],"date":["Thu, 30 Oct 2014 10:33:31 GMT"],"content-type":["text/html; charset=UTF-8"]}
[Status] 200
* I see response code is 200
* I see response contains "test ok"
FAIL
---
Couldn't see response contains "test ok":
REST response contains
Failed asserting that '' contains "test ok".
Scenario Steps:
3. I see response contains "test ok"
2. I see response code is 200
1. I send get "/"
Is this down to the URL being available to me (not the VM) on that address? If I visit that URL, it does open with "test ok"!
@alias I believe this has to do with the URL not being available due to the VM. I have recently had some problems with Homestead and Acceptance testing and am currently tracking that down. Did you try the setup they have currently on the test app?
I'm noticing they don't have a URL setup for the config in this app.
Yeah it seems as though they would just have that running locally (on their computer, not the VM).
This is annoying though, it seems the only way to test this is via my own computer, which kinda defeats the point of having the VM in my eyes.
Returning to the original question:
I understand you can mock some Laravel Facades with shouldReceive, but when I have my own Facades, what happens then?
If your facade is just a thin wrapper which returns implementation resolved through Laravel DI, then it can be mocked the same way as Laravel's facades. You just should avoid adding any methods to the facade itself except that very special getFacadeAccessor method. If you have added any other methods, than it is not facade pattern anymore - facades should generally just be facades to some specific implementation.
Here is an example:
namespace App\Services\Facades;
use Illuminate\Support\Facades\Facade;
class MyServiceFacade extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return "my.service";
}
}
Implementation which will be "facaded":
namespace App\Services;
class MyService
{
public function doIt()
{
return "MyService->doIt was called for real";
}
}
And in register method in one of yor service providers:
// this time registered as singleton instance,
// but you can register it as you wish - as singleton instance
// or as a function to create a new instance for every call
$this->app->instance("my.service",
new \App\Services\MyService());
Now lets write some code which will use our facade:
namespace App\Services;
use App\Services\Facades\MyServiceFacade;
class MyDependentService
{
public function doItThroughFacade()
{
// calling as static function from our service instance
$result = MyServiceFacade::doIt();
return $result;
}
}
And then in your test (I'm using Codeception):
public function testMyServiceDoIt()
{
// the mock creation & registration would be better in a test setup method
App\Services\Facades\MyServiceFacade::shouldReceive("doIt")
->once()
->andReturn("The facade call was mocked");
// run
$testSubject = new App\Services\MyDependentService();
$result = $testSubject->doItThroughFacade();
// verify
$this->tester->assertEquals("The facade call was mocked",
$result,
"The result must be mocked");
}
Essentially, Laravel is setting up some things behind the scenes, so your mocks are set for you as something like following (you can actually replace that shouldReceive block with this code and it whoucl work the same way):
$myServiceMock =
Mockery::mock(\App\Services\MyService::class);
app()->instance("my.service", $myServiceMock);
// set up expectations
$myServiceMock->shouldReceive("doIt")
->once()
->andReturn("The facade call was mocked");
Essentially, facades can be tested if done right. You just should avoid adding any implementation to them and let Laravel do its "magic" DI stuff.
Please or to participate in this conversation.