Setting up PHPUnit tests in Laravel
Asked Answered
L

1

16

I'm fairly new to unit testing, but I've read pretty much all the documentation on phpunit.de (Up to chapter 10).

It states that testing using databases can be slow, but if setup correctly, it can be just as fast as non-database testing.

As such, I want to test a model in Laravel. I've created a model factory to seed data into the database.

I've also created a basic test.

In PHPUnits documentation, it states that before every test, the setUp() method is called to setup the test. There's also another static method setUpBeforeClass().

I want to seed my database table only once, and use the records within my test. So I used Laravels factory() function to seed the database from within the setUpBeforeClass() method.

This is my code:

class CommentTest extends TestCase
{
    protected static $blog;
    protected static $comments;

    public static function setUpBeforeClass()
    {
        parent::setUpBeforeClass();

        self::$blog = factory(App\Models\Content\Blog::class)->create();
        self::$comments = factory(App\Models\Content\Comment::class, 6)->create();
    }

    public function testSomething()
    {
        $this->assertTrue(true);
    }
}

However, when I run phpunit, I get the following error:

Fatal error: Call to a member function make() on a non-object in \vendor\laravel\framework\src\Illuminate\Foundation\helpers.php on line 54

Call Stack:
    0.0002     240752   1. {main}() \vendor\phpunit\phpunit\phpunit:0
    0.0173    1168632   2. PHPUnit_TextUI_Command::main() \vendor\phpunit\phpunit\phpunit:47
    0.0173    1175304   3. PHPUnit_TextUI_Command->run() \vendor\phpunit\phpunit\src\TextUI\Command.php:100
    2.9397    5869416   4. PHPUnit_TextUI_TestRunner->doRun() \vendor\phpunit\phpunit\src\TextUI\Command.php:149
    2.9447    6077272   5. PHPUnit_Framework_TestSuite->run() \vendor\phpunit\phpunit\src\TextUI\TestRunner.php:440
    2.9459    6092880   6. PHPUnit_Framework_TestSuite->run() \vendor\phpunit\phpunit\src\Framework\TestSuite.php:747
    2.9555    6096160   7. call_user_func:{\vendor\phpunit\phpunit\src\Framework\TestSuite.php:697}() \vendor\phpunit\phpunit\src\Framework\TestSuite.php:697
    2.9555    6096272   8. CommentTest::setUpBeforeClass() \vendor\phpunit\phpunit\src\Framework\TestSuite.php:697
    2.9555    6096480   9. factory() \tests\CommentTest.php:18
    2.9556    6096656  10. app() \vendor\laravel\framework\src\Illuminate\Foundation\helpers.php:350

If I move the code from setUpBeforeClass() to setUp() and run it, it works as expected, but surely this is inefficient as its seeding the database for every test?

My questions:

  1. Is seeding the database from within the setUpBeforeClass() the correct way to do this?
  2. If it is (question 1), then why am I getting the fatal error when running phpunit, and is there anything I should be doing before calling factory()?
  3. If I do have to place the code in the setUp() method, are there going to be performance issues?
  4. Should I even be seeding from the setUpBeforeClass() or setUp() methods? In Laravels documentation it shows examples where the seeding is happening in the test itself, but If i'm running 100 tests (for example), is it a good idea to be seeding 100 times?
Luettaluevano answered 17/11, 2015 at 10:3 Comment(0)
L
28

Ok, after a bit of investigating (the classes), I've determined that the Laravel application has not yet been created when the static setUpBeforeClass() method is called.

The Laravel container is created the first time setUp() is called in \vendor\laravel\framework\src\illuminate\Foundation\Testing\TestCase.php. That's why it works fine when I move my code to the setUp() method.

The container is then stored in the $app property stored in \vendor\laravel\framework\src\illuminate\Foundation\Testing\ApplicationTrait.php.

I can manually create a container instance by adding this code to the setUpBeforeClass() method:

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

But this method seems pretty hacky, and I don't like it.

Instead, I moved the seeding code to the setUp() method, but only seeded the database if the class properties were null. Therefore it only gets seeded on the first call of setUp(). Any subsequent calls do not get seeded:

class CommentTest extends TestCase
{
    use DatabaseMigrations;

    protected static $blog;
    protected static $comments;

    public function setUp()
    {
        parent::setUp();

        $this->runDatabaseMigrations();

        if (is_null(self::$blog)) {
            self::$blog = factory(App\Models\Content\Blog::class, 1)->create();
            self::$comments = factory(App\Models\Content\Comment::class, 6)->create();
        }
    }
}

In combination with Laravels DatabaseMigrations trait for testing, This is now the workflow:

  1. PHPUnit is called
  2. The Test class is called, which contains the DatabaseMigrations trait
  3. The database is migrated (tables created)
  4. The setUp() method is called for the first time, which seeds the relevant tables with testing data
  5. The test is run, and and access the test data
  6. There is no tearDown() method invoked, instead the DatabaseMigrations trait simply resets the database, so my test doesn't have to worry about cleaning up the test data.

EDIT

In addition, it seems (although I'm not 100%), that if you have a custom setUp() method, you need to manually call runDatabaseMigrations() from the overridden setUp() method:

public function setUp()
{
    parent::setUp();
    $this->runDatabaseMigrations();

    /** Rest of Setup **/
}

runDatabaseMigrations() doesn't seem to get called automatically if you overload the setUp() method.

I hope this helps, but if anyone else has a better solution, please feel free to let me know :)

Luettaluevano answered 17/11, 2015 at 10:45 Comment(10)
Phil, any ideas why "runDatabaseMigrations" doesn't get triggered when setUp is overloaded? I have looked through the code and cannot seem to fathom why?Moya
@Moya Unfortunately I'm not sure why! (If my assumption is even correct). I've had a look through the code as well and can't figure it out either :( This issue is also referenced in this question: #33584571Luettaluevano
Thank You!! I had just run into this issue myself and was wondering why the application was not setup prior to setUpBeforeClass. I was tracing the code as well and about to create a SO question myself until I found this :)Alcheringa
@JosephCrawford glad it helps :)Luettaluevano
The only problem I found is when using the setUp method to create factory models with faker data events that are thrown are no longer seen as thrown when they are. I have added debugging code to my listener and it is triggered, yet expectsEvents thinks not.Alcheringa
#36090925Alcheringa
I started to test the runDatabaseMigrations problem mentioned, but as noted github.com/laravel/framework/commit/… it has been fixed in 5.2Starr
@DustinGraham Nice! Thanks for the comment :)Luettaluevano
I can't thank you enough for this post! It helped me solve my problem here: https://mcmap.net/q/749084/-behat-hangs-when-there-are-multiple-scenarios-but-works-on-a-single-oneCorrection
Thanks again from 2019! Was scratching my head with ths one - but this solved itGuenna

© 2022 - 2024 — McMap. All rights reserved.