22

I have something like the following set up in Laravel:

In /app/controllers/MyController.php:

class MyController extends BaseController { const MAX_FILE_SIZE = 10000; // .... } 

In /app/tests/MyControllerTest.php:

class MyControllerTest extends TestCase { public function myDataProvider() { return [ [ MyController::MAX_FILE_SIZE ] ]; } /** * @dataProvider myDataProvider */ public function testMyController($a) { // Just an example $this->assertTrue(1 == 1); } } 

However, when I run vendor/bin/phpunit I get the following error:

 PHP Fatal error: Class 'Controller' not found in /home/me/my-app/app/controllers/BaseController.php on line 3 Fatal error: Class 'Controller' not found in /home/me/my-app/app/controllers/BaseController.php on line 3 

If I remove the reference to the MyController class in myDataProvider() and replace it with a literal constant then the test completes successfully.

In addition, I can place references to MyController::MAX_FILE_SIZE inside the actual testMyController() method, and the test also completes successfully.

It appears that the autoloading setup for Laravel framework classes isn't being set up until after the data provider method is being called, but before the actual test methods are called. Is there any way around this so that I can access Laravel framework classes from within a PHPUnit data provider?


NOTE: I'm calling PHPUnit directly from the command line and not from within an IDE (such as NetBeans). I know some people have had issues with that, but I don't think that applies to my problem.

4 Answers 4

31

As implied in this answer, this appears to be related to the order that PHPUnit will call any data providers and the setUp() method in any test cases.

PHPUnit will call the data provider methods before running any tests. Before each test it will also call the setUp() method in the test case. Laravel hooks into the setUp() method to call $this->createApplication() which will add the controller classes to the 'include path' so that they can be autoloaded correctly.

Since the data provider methods are run before this happens then any references to controller classes inside a data provider fail. It's possible work around this by modifying the test class to something like this:

class MyControllerTest extends TestCase { public function __construct($name = null, array $data = array(), $dataName = '') { parent::__construct($name, $data, $dataName); $this->createApplication(); } public function myDataProvider() { return [ [ MyController::MAX_FILE_SIZE ] ]; } /** * @dataProvider myDataProvider */ public function testMyController($a) { // Just an example $this->assertTrue(1 == 1); } } 

This will call createApplication() before the data provider methods are run, and so there is a valid application instance that will allow the appropriate classes to be autoloaded correctly.

This seems to work, but I'm not sure if it's the best solution, or if it is likely to cause any issues (although I can't think of any reasons why it should).

Sign up to request clarification or add additional context in comments.

2 Comments

It's probably safer to call $this->refreshApplication();, as this also sets the environment to testing. You also don't necessarily need to create a constructor, you could just call $this->refreshApplication(); directly in your data provider (if you just have the one I guess).
Antonio's comment should be considered, as it is much faster (in a big app) and consumes less memory. I had "memory limit" issue with the accepted answer
20

The test will initialize much faster if you create the application right within the dataProvider method, especially if you have large set of items to test.

public function myDataProvider() { $this->createApplication(); return [ [ MyController::MAX_FILE_SIZE ] ]; } 

Comments

6

Performance warning for the other solutions (especially if you plan to use factories inside your dataProviders):

As this article explains:

The test runner builds a test suite by scanning all of your test directories […] When a @dataProvider annotation is found, the referenced data provider is EXECUTED, then a TestCase is created and added to the TestSuite for each dataset in the provider.

[…]

if you use factory methods in your data providers, these factories will run once for each test utilizing this data provider BEFORE your first test even runs. So a data provider […] that is used by ten tests will run ten times before your first test even runs. This could drastically slow down the time until your first test executes. Even […] using phpunit --filter, every data provider will still run multiple times. Filtering occurs after the test suite has been generated and therefore after any data providers have been executed.

The above article proposes to return a closure from the dataProvider and execute that in your test:

/** * @test * @dataProvider paymentProcessorProvider */ public function user_can_charge_an_amount($paymentProcessorProvider) { $paymentProcessorProvider(); $paymentProcessor = $this->app->make(PaymentProviderContract::class); $paymentProcessor->charge(2000); $this->assertEquals(2000, $paymentProcessor->totalCharges()); } public function paymentProcessorProvider() { return [ 'Braintree processor' => [function () { $container = Container::getInstance(); $container->bind(PaymentProviderContract::class, BraintreeProvider::class); }], ... ]; } 

2 Comments

$this->refreshApplication() is not helping in Laravel 7. I used closure as you said, and I was able to call factory. Thanks
Thanks! This helped in my case, I needed access to a config value inside of a dataprovider and with the performance concerns this seems to be the best approach.
6

LATEST EDIT:

Laravel News now has an article on this exact topic. It suggests to wrap the Laravel functionality in a Closure within the data provider:

class ExampleTest extends TestCase { use RefreshDatabase; #[DataProvider('nonAdminUsers')] public function test_non_admin_users_cannot_access_admin($user): void { $response = $this ->actingAs($user()) ->get('/admin') ->assertStatus(403); } public static function nonAdminUsers(): array { return [ [fn(): User => User::factory()->player()->create()], [fn(): User => User::factory()->coach()->create()], [fn(): User => User::factory()->owner()->create()], ]; } } 

The bad solution, see edit below:

You can adjust this behaviour of PHPUnit by adding your custom bootstrapper to your project's phpunit.xml like this (look at 3rd line):

<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" ← ← ← THIS xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> ... </phpunit> 

Then create a bootstrap.php file in your tests folder (i.e. the path you denoted above), and paste this:

<?php use Illuminate\Contracts\Console\Kernel; require __DIR__ . '/../vendor/autoload.php'; $app = require __DIR__.'/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); 

You can now use Laravel functionality in your data providers, just keep in mind they still run after your setUp methods.

EDIT AFTER 3 YEARS WITH THIS IN PRODUCTION:

TL;DR don't use this hack!

Running PHPUnit 10.5.* we noticed that each time this test was run - the outputted number would increment:

public function test_asd(): void { dump(\App\Models\RealModelWeUse::count()); } 

It turns out when running phpunit --filter test_asd PHPUnit also loads all tests first and then generates all cases for every dataprovider. If any of them used factories/created DB entries, these stayed permanently, even disregarding use DatabaseTransactions;.

Somehow, running the test via PHPStorm which does this:

php /var/www/vendor/phpunit/phpunit/phpunit --configuration /var/www/phpunit.xml --filter "/(Tests\\App\\Http\\Controllers\\OrderControllerTest::test_asd)( .*)?$/" --test-suffix OrderControllerTest.php /var/www/tests/app/Http/Controllers --teamcity 

Does not create the surplus entries. I was not motivated enough to find out why to be frank.

So this is definitely a hack around expected behaviour and it was bound to have some side effects, and there they are :)

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.