Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

sfsccn's avatar

Seeding/unseeding the database once per test class

The Problem

I’d like to use setUpBeforeClass() rather than setUp() to seed my database for a test class. This is because I don’t want the seeding/unseeding to be repeated for every test in the class. The problem is that in setUpBeforeClass() calling parent::setUpBeforeClass() doesn’t seem to set up the app and so database operations fail with an error like “Call to member function connection() on null”. In fact, I can’t find a definition of setUpBeforeClass() in the Laravel code; for example, it’s not defined on Illuminate/Foundation/Testing/TestCase. And you can’t call parent::setUp() in static methods.

The Workaround - Does This Make Sense?

I did find the workaround below. In setUpBeforeClass() and tearDownAfterClass() I just instantiate the test class and call setUp() to set up the app. Note that I’m not actually creating a seed for the test, I just do a single create at the beginning and a delete at the end.

I’m not sure this method of using setUpBeforeClass() and tearDownAfterClass() makes sense. Suggestions welcome.

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\AircraftManufacturer;

class AircraftManufacturerTest extends TestCase
{
    /**
     * Name to use in test model
     * 
     * @var string 
     */
    public const NAME = 'AircraftManufacturerTest';

    /**
     * Because there's no setUpBeforeClass() method defined on
     * Tests\Feature\TestCase we need to somehow call the parent setUp() in
     * order to set up the app. The following seems to work.
     *
     * @return void
     */
    public static function parentSetUp() {
        (new AircraftManufacturerTest())->setUp();
    }

    public static function setUpBeforeClass(): void
    {
        self::parentSetUp();
        AircraftManufacturer::create(['name' => self::NAME]);
    }

    /**
     * Check test model created as expected
     * 
     * @return void
     */
    public function testGetModel()
    {
        $m = AircraftManufacturer::where('name', self::NAME)->first();
        $this->assertEquals(self::NAME, $m->name, '');
    }

    /**
     * Check timestamps are cast to Carbon
     * 
     * @return void
     */
    public function testTimestampsCastToCarbon()
    {
        $m = AircraftManufacturer::where('name', self::NAME)->first();
        $this->assertInstanceOf('Illuminate\Support\Carbon', $m->created_at);
    }

    public static function tearDownAfterClass(): void
    {
        self::parentSetUp();
        AircraftManufacturer::where('name', self::NAME)->first()->delete();
    }
}

Note that another workaround is to create the model in the first test, pass it along via @depends and then delete it in the last test. But I’d prefer to use setUpBeforeClass() and tearDownAfterClass() if possible.

My environment

  • PHP 7.3.11
  • Laravel 7.5.2
  • PHPUnit 9.1.1
0 likes
5 replies
m7vm7v's avatar

You could resolve all that in a few simple steps -

  1. Use Illuminate\Foundation\Testing\RefreshDatabase trait - that will make sure that whatever is created in your tests it will be deleted after execution for you automatically without any setUp calls in the beginning and the end... Also it won't even touch your database (make sure for testing you setup different DB in the phpunit.xml)

  2. Make a Factory for the tested Model - https://laravel.com/docs/7.x/database-testing#writing-factories

  3. I wouldn't test that name == name or created_at is casted as those are assumed that the framework has tested itself that these will be truth, furthermore in a matter of sense these are not even Featured tests but more like Unit tests.

  4. I would test the Feature that when I have a $manufacturer = factory(AircraftManufacturer::class)->make()->toArray; and hit a route post(/manufacturers, $manufacturer) then I will assert that in the DB i will have a record with that name.

1 like
sfsccn's avatar

Hi @m7vm7v. Thanks for the suggestions, these are helpful. I'm just learning, hence the somewhat nonsensical casting test.

In #1, the automatic reset via RefreshDatabase could be useful, but are you sure that should enable models to be created in setUpBeforeClass()? Also, that seems to delete all the data in the database, which isn't exactly what I want, I just want to delete any data created in the test.

When I add this trait to the test class above, I get errors like this which refers to $this->assertEquals(self::NAME, $m->name, '') in testGetModel()

1) Tests\Feature\AircraftManufacturer2Test::testGetModel
ErrorException: Trying to get property 'name' of non-object

AircraftManufacturer2Test.php:60

I think my issue may be a feature request. Namely that in addition to setUp and tearDown, my request would be that Illuminate\Foundation\Testing\TestCase also provide the static methods setUpBeforeClass and tearDownAfterClass. The result should be that by replacing the hack self::parentSetUp() in the above code with parent::setUpBeforeClass() it would work. I believe a key step in these is bootstrapping the app. For example, this also works:


use Illuminate\Contracts\Console\Kernel;
...
    /**
     * Bootstrap the app. A key step in the non-static Tests\Feature\TestCase::setUp().
     * 
     * @return void
     */
    public static function boostrapApp()
    {
        $app = require __DIR__ . '/../../../bootstrap/app.php';
        $app->make(Kernel::class)->bootstrap();
    }

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass(); // The empty PHPUnit method that does nothing
        self::boostrapApp(); // Rather than parentSetUp()
        AircraftManufacturer::create(['name' => self::NAME]);
    }

    public static function tearDownAfterClass(): void
    {
        parent::tearDownAfterClass(); // The empty PHPUnit method that does nothing
        self::boostrapApp(); // Rather than parentSetUp()
        $m = new AircraftManufacturer();
        $m->where('name', self::NAME)->first()->delete();
    }

In general, rather than adding models in the test itself, I suspect it's better to simply rely on separate seed scripts like database/seeds/AircraftManufacturerSeeder.php. However, I imagine there may be times when one would want to create a very specific model directly in a test. It would be nice to be able to do this once per class using setUpBeforeClass and tearDownAfterClass. But perhaps the developers felt that for some reason this is not a good idea. There's a somewhat related issue here.

sfsccn's avatar

Those articles look good, thanks!

m7vm7v's avatar

If there's been a reply that has been helpful for answering your query can you please make sure you mark it as best reply so we can help any future devs stuck on the same situation.

Please or to participate in this conversation.