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

marcel158's avatar

Use .env.testing for phpunit

When I run unit tests I do operations on the database, combinated with the RefreshDatabase trait. However to not mess up with my application database, I created a file .env.testing. Here I could set the DB_NAME to myapp_test and in the future I might wanna set up different api keys here. So I think it is a good idea to work with different .env files per environment (in this case the testing environment)?

How do I tell Laravel now to actually use this env file, instead of .env, when running unit tests. Always.

0 likes
9 replies
Talinon's avatar

@marcel158 Within your phpinit.xml you can set up your environment configuration.

For example, using an in-memory database for your testing (which is a great option) might look like this:

  <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="memory_testing"/>
        <env name="DB_DATABASE" value=":memory:"/>
    </php>

Then just set up the memory_testing database connection within config/database.php

        'memory_testing' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ],

2 likes
gazd1977's avatar

I believe you update the values in your phpunit.xml file to overwrite the APP_ENV definition.

    <php>
        <env name="APP_ENV" value="testing" force="true" />
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
    </php>
marcel158's avatar

Thank you both.

But the setting

<env name="APP_ENV" value="testing" />

is already set. I believe by default. It does not work and I wouldn't expect it to work. I think this will just set the environment variable APP_ENV to testing, not using the environment variables from .env.testing. But this got me the idea to just set the DB_DATABASE in this file. That will work for now at least.

Thanks for the tip with the in-memory database. I will think about it. But I think it's a good idea to use the same database connection that you use in your application. There are still differences between e.g. mysql and sqlite at the end.

Talinon's avatar

But I think it's a good idea to use the same database connection that you use in your application

I would argue against that. Using an in memory database means that for each unit test, you are using a fresh slate - get into the habit of using factories for each test. With a unit test, you are testing a single function or feature; so you don't need a fully populated database. Just create the bare minimum for what you need to run the test. There are situations where an ongoing populated database could affect your tests, if you're not careful. An in memory database is also lightening fast.

You are correct that there are some gotchas when using an sqlite database, but I rarely come across issues that make it a show stopper; there is usually a work around.

1 like
grobogryz's avatar

@Talinon sqlite differs from mysql and doesn't support all its capabilities. on the other hand you can clear your mysql DB same way as sqlite DB before each test.

Talinon's avatar

Of course, but that also means you're clearing your local environment every time you run a test.

I keep a local environment database (such as MySQL) separate from the testing database (usually in memory)

You can have the best of both worlds. The set up I explained with phpunit.xml will provide this.

1 like
marcel158's avatar

Yeah, but thats why I work with two databases. myapp and myapp_test.

1 like
agm1984's avatar

In my opinion, the optimal solution is to use the DatabaseTransactions trait with unit tests and use your real database. It allows you to make assertions on the database during unit tests, but the transactions are not commited, so the data/transaction is rolled back after each test.

To accomplish this, you simply maintain your .env file as normal, and then use the phpunit.xml file to specify testing environment variables.

To use database transactions, you simply omit the DB_CONNECTION and DB_DATABASE variables, and it will use the ones from your .env file.

Here is my basic setup for your R&D:

phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="AppLayout">
            <directory suffix="Test.php">./tests/AppLayout</directory>
        </testsuite>
        <testsuite name="Auth">
            <directory suffix="Test.php">./tests/Auth</directory>
        </testsuite>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <listeners>
        <listener class="\Mockery\Adapter\Phpunit\TestListener"
                  file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php">
        </listener>
    </listeners>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <!-- <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/> -->
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>

It is important to note that the goal with unit testing is to test your real setup as closely as possible because the purpose of testing is to ensure your real setup is working.

If you use SQL Lite, it requires different syntax and isn't your real database, so such methodology immediately deviates from the idea of testing against what is actually used.

TestCase.php

<?php

namespace Tests;

use App\User;
use Illuminate\Auth\SessionGuard;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Spatie\Permission\Models\Role;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseTransactions;

    const AUTH_PASSWORD = 'password';

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

    public function tearDown() : void
    {
        parent::tearDown();
    }

    protected function resetAuth(array $guards = null) : void
    {
        $guards = $guards ?: array_keys(config('auth.guards'));

        foreach ($guards as $guard) {
            $guard = $this->app['auth']->guard($guard);

            if ($guard instanceof SessionGuard) {
                $guard->logout();
            }
        }

        $protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
        $protectedProperty->setAccessible(true);
        $protectedProperty->setValue($this->app['auth'], []);
    }

    /**
     * Creates and/or returns the designated admin user for unit testing
     *
     * @return \App\User
     */
    public function adminUser() : User
    {
        $user = User::query()->firstWhere('email', '[email protected]');

        if ($user) {
            return $user;
        }

        $user = User::generate('Test Admin', '[email protected]', self::AUTH_PASSWORD);

        $user->assignRole(Role::findByName('admin'));

        return $user;
    }

    /**
     * Creates and/or returns the designated regular user for unit testing
     *
     * @return \App\User
     */
    public function user() : User
    {
        $user = User::query()->firstWhere('email', '[email protected]');

        if ($user) {
            return $user;
        }

        $user = User::generate('Test User', '[email protected]', self::AUTH_PASSWORD);

        return $user;
    }
}

Then you can run complex test such as this:

    /** @test */
    public function it_can_create_new_user_from_twitter_identity()
    {
        $twitter_identity = [
            'id' => '123',
            'name' => 'New Twitter User',
            'email' => '[email protected]',
            'token' => 'access-token',
            'refreshToken' => null,
        ];

        $this->mockTwitterOAuth($twitter_identity);

        $this->withoutExceptionHandling();

        $this->get(route('oauth.callback', 'twitter'))
            ->assertText('token')
            ->assertSuccessful();

        $this->assertDatabaseHas('users', [
            'name' => $twitter_identity['name'],
            'email' => $twitter_identity['email'],
        ]);

        $this->assertDatabaseHas('oauth_providers', [
            'user_id' => User::query()->firstWhere('email', $twitter_identity['email'])->id,
            'provider' => 'twitter',
            'provider_user_id' => $twitter_identity['id'],
            'access_token' => $twitter_identity['token'],
            'refresh_token' => $twitter_identity['refreshToken'],
        ]);
    }

And then you can examine your database immediately after and notice that it was used but no new data is persisted.

That resetAuth method is key for destroying session between tests. You can see its power for a test such as this:

    /** @test */
    public function it_should_throw_error_429_when_login_attempt_is_throttled()
    {
        $this->resetAuth();

        $throttledUser = factory(User::class, 1)->create()->first();

        foreach (range(0, 9) as $attempt) {
            $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => "{TestCase::AUTH_PASSWORD}_{$attempt}"]);
        }

        $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => 'k'])
            ->assertStatus(429)
            ->assertJson(['message' => 'Too Many Attempts.']);

        $this->resetAuth();
    }

Without resetAuth, earlier authentication-related unit tests will reuse AuthManager's cached auth guard data, so you will notice that throttling kicks in earlier. With resetAuth, you will notice it triggers exactly according to your definition :

// the above unit test will trigger the 429 on the 11th attempt
Route::group(['middleware' => ['guest', 'throttle:10,5']], function () {});

I consider the resetAuth function extremely important, so I will show it here. I also consider it important to read this post from the source from whom I acquired the function: https://stackoverflow.com/questions/57813795/method-illuminate-auth-requestguardlogout-does-not-exist-laravel-passport

If you copy my TestCase.php, the DatabaseTransactions trait is what makes it use the database defined in your .env file with transaction wrappers.

Then, in each test file, you just need to use it:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;

class OAuthTwitterTest extends TestCase
{
    use DatabaseTransactions;

    // test
}

For extremely advanced use cases, you can research how to manually commit and rollback transactions.

Please or to participate in this conversation.