7

I have a lot of tests and running all of them takes a long time ~15 minutes. This is mainly due to a lot fo the tests building a new sqlite database and seeding it.

A lot of my tests don't change the database, so they could all be run on the same database, which is created just once. However, I can't figure out how to setup my tests to work like this.

I use an in memory sqlite, in Laravel.

I am trying to stop my phpunit tests from creating and seeding the database every time.

My latest attempt is to use the trait detailed here: https://stackoverflow.com/a/57788123/42106

However, when I run my tests, the first test passes fine (so the database tables exist) then the 2nd test in the file fails with: "General error: 1 no such table: users".

./bin/phpunit ./tests/Auth/UserTest.php

So the database tables have been wiped after the first test.

I have tried overriding the tearDown method but it makes no difference.

What could be wiping my database?

<?php

namespace Tests\Auth;

use Tests\TestCase;
use Tests\MigrateFreshAndSeedOnce;
use App\Entity\Models\User;

class UserTest extends TestCase
{

    use MigrateFreshAndSeedOnce;

    public function testUser1()
    {
        $user = User::where('id', 1)->get()->first();
        $this->assertTrue($user->id !== null);
    }

    public function testUser2()
    {
        $user = User::where('id', 2)->get()->first();
        $this->assertTrue($user->id !== null);
    }
}

Here is the trait:

<?php

namespace Tests;

use Illuminate\Support\Facades\Artisan;

trait MigrateFreshAndSeedOnce
{

    /**
     * If true, setup has run at least once.
     *
     * @var boolean
     */
    protected static $setUpHasRunOnce = false;

    /**
     *
     * @return void
     */
    public function setUp() : void
    {
        parent::setUp();

        if (!static::$setUpHasRunOnce) {

            echo '--DB--';
            Artisan::call('migrate:fresh');

            Artisan::call(
                'db:seed',
                ['--class' => 'DatabaseSeeder'] //add your seed class
            );

            static::$setUpHasRunOnce = true;
        }
    }
}

Finally, my TestCase class:

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected $baseUrl = 'http://dev.php73.mysite.com:8888';
}

My phpunit env vars:

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="testing" />
    </php>

Thanks!

7
  • 1
    In the default configuration (i.e. if you uncommented these lines) the default sqlite test database is an in memory database so it would be wiped between tests. Commented Jan 25, 2022 at 15:26
  • Yes, I want to have it in memory and persist between tests. Is that possible? Commented Jan 25, 2022 at 15:54
  • 1
    as far as I know each test is run in its own PHP process so there's no memory sharing possible Commented Jan 25, 2022 at 16:47
  • Why won't you use use RefreshDatabase; trait? Why do you need to persist the data in memory/sharing? Commented Jan 25, 2022 at 17:52
  • Just want it to run quickly. I have hundreds of tests so there’s no need to create, seed and destroy the full database every time Commented Jan 25, 2022 at 19:10

2 Answers 2

-1

as @matiaslauriti stated in the comments, with Laravel 8, you should use the RefreshDatabase trait. Once applied it will look for a boolean property called seed.

Give your test class the protected property $seed and set it to true if you wish to seed and move tests that don't need a refresh to another test class.

class ProjectControllerTest extends TestCase
{

    protected $seed = true;
}

Then only seed necessary tests will build the database.

Alternatively use:

$this->seed(); 

inside a test method, to seed for the specific test.

check out the docs on it

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

1 Comment

This will help if there are seeders involved, but what they're looking to avoid is the database rebuild with each test. This will still run migrate:fresh for every test.
-2

You're almost there believe it or not...

Change the name of the setUp() method in your MigrateFreshAndSeedOnce trait to setUpOnce() like so...

<?php

namespace Tests;

use Illuminate\Support\Facades\Artisan;

trait MigrateFreshAndSeedOnce
{

    /**
     * If true, setup has run at least once.
     *
     * @var boolean
     */
    protected static $setUpHasRunOnce = false;

    /**
     *
     * @return void
     */
    public function setUpOnce() : void
    {
        parent::setUp();

        if (!static::$setUpHasRunOnce) {

You just need to implement the setUp method on each test case class (which does the magic with the trait) like so...

<?php

namespace Tests\Auth;

use Tests\TestCase;
use Tests\MigrateFreshAndSeedOnce;
use App\Entity\Models\User;

class UserTest extends TestCase
{

    use MigrateFreshAndSeedOnce;

    protected function setUp(): void
    {

        $this->setUpOnce();

    }

    public function testUser1()
    {
        $user = User::where('id', 1)->get()->first();
        $this->assertTrue($user->id !== null);
    }

    public function testUser2()
    {
        $user = User::where('id', 2)->get()->first();
        $this->assertTrue($user->id !== null);
    }
}

If you don't want to add that manually to every test case class, perhaps you could add it to your abstract class TestCase and it might apply automagically to all test case classes?

6 Comments

Where is setUpOnce() defined? What do you think it should do?
Sorry, good catch - answer updated.
And it means that we will call the setUpOnce method from the trait (where the magic happens) as opposed to the one in the BaseTestClass
That’s not how inheritance works. If you define a setup() method in the child class it overrides the one in the parent class.
OP. Just give it a try. Trust me, it works.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.