framework: [5.0] DB transactions don't work in tests (same way as in L4)

Easiest way to reproduce

  • configure correct .env file
  • run the following test
<?php

class ExampleTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();
        echo PHP_EOL.'Before opening transaction... ';
        DB::beginTransaction();
        echo 'Transaction opened'.PHP_EOL;
    }

    public function tearDown()
    {
        parent::tearDown();
        echo 'Before closing transaction... ';
        DB::rollBack(); // <-- fails here
        echo 'Transaction closed'.PHP_EOL;
    }

    public function testBasicExample()
    {
    }
}

Output: Before opening transaction… Transaction opened Before closing transaction…

Time: 84 ms, Memory: 13.00Mb

There was 1 error:

  1. ExampleTest::testBasicExample ReflectionException: Class config does not exist

What I found is that before rollBack() Illuminate\Container\Container->instances had an array of various instances but on rollBack() it was empty which caused

Cannot use object of type Illuminate\Support\Facades\Config as array in ...../vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php on line 259

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 19 (5 by maintainers)

Most upvoted comments

You’re probably doing the tests wrong then. Ask on the forums.

This is actually totally unrelated. This is caused by the ioc container not being initialised properly by your test suite.

After literal days spent trying to figure out why some data being created in one test was being persisted through to another test (tried using both DatabaseTransactions and RefreshDatabase), I finally got our tests/database to actually reflect what I wanted by following OP’s suggestion. Moving the parent::tearDown to after the DB::rollback was actually key here.

I think the folks at @laravel-internals/@laracasts seriously need to reflect on their DatabaseTransactions and RefreshDatabase code and documentation. Don’t just shrug it off as “this is unrelated” or some other BS, as clearly there is something going on here that’s actually affecting people. The documentation around those traits is so nebulous it’s almost funny. “The RefreshDatabase trait takes the most optimal approach” …ok, and how? “Use the trait on your test class and everything will be handled for you” …ok, it doesn’t? Not to mention the example code is just the use XXX at the top of the class, which is far from helpful when it doesn’t actually handle it for you.

Just for some background on what I wasted my time on, all I wanted was DatabaseTransactions to work, so I could run migrations once per test file, and seed some custom data once for each test file. This isn’t possible using DatabaseMigrations, as after each migration you would need to re-seed any test data, which can get extremely expensive if your Test suite needs a lot of test data and/or you have a lot of tests.

Everything worked with DatabaseMigrations (which makes sense as it basically drops all tables before starting fresh), but when I simply replaced with DatabaseTransactions/RefreshDatabase, everything went to shit. Some tests would pass, while others wouldn’t. Sometimes data persisted between tests (I could straight up call a factory to create a record, and query it from within the next test run – am I missing something here, or how could this ever work unless the DatabaseTransactions trait wasn’t doing its job), while other times it wouldn’t. Connection was never a problem, even though this is the oft-diagnosed problem, and even when using a non-default MySQL connection.

Like I said, I’ve finally gotten the tests/database to behave in a sensible way by skipping all of the provided Laravel traits, and using the DB::beginTransaction and DB::rollBack methods within the setUp and tearDown functions, along with single-run custom initTest() function that only gets run on the first setUp so that we can only run the seeders once. This was all done on a basically fresh Laravel app.

What? Both statements you have there are identical under the hood, only the first one is more correct.

I found on my L5 instance this was happening due to the way the config file gets loaded in the DatabaseManager in tests.

For instance:

$connection->setFetchMode($this->app['config']['database.fetch']);

Instead of:

$connection->setFetchMode(\Config::get('database.fetch'));

Not sure if I am just initializing it wrong as well. But when I change it to this everything runs fine. Tests and the application.

Should this be reopened or moved to a new ticket?

The container can’t know that what’s its returning isn’t what you want. It’s not that magic I’m afraid.

@GrahamCampbell How it should be initialized? I’ve been porting real tests from L4 to L5 and proposed ‘fix’ works

//        parent::tearDown(); // <-- if uncommented instead of the last line tests will fail
        DB::rollBack();
        DB::disconnect();
        Mockery::close();
        parent::tearDown();

NB It works if parent::tearDown(); moved down the function howerver this code worked fine in L4