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.