Testing Database Interactions in Laravel

Testing database interactions is one of the most important parts of application testing. In Laravel, the database plays a central role in almost every feature—user authentication, orders, posts, products, payments, and much more. Ensuring that database operations work correctly is essential to building stable, predictable, and bug-free applications. Laravel provides powerful tools for testing database queries, insertions, updates, deletions, and model behavior using an in-memory SQLite database or a dedicated test database.

This extensive 3000-word guide will explain everything you need to know about testing database interactions in Laravel. You will learn how Laravel structures database tests, how to use testing traits like RefreshDatabase, how to make use of model factories, how to assert database state, how to test insertions and deletions, how to test relationships, how to fake transactions, how to test validation with the database, how to test migrations, how to use Pest or PHPUnit for database tests, how to isolate test cases, and best practices for building clean and reliable database test suites.

By the end of this post, you will be able to write professional-level database tests for any Laravel application.

Understanding Why Database Testing Is Important

Database interactions are often responsible for the most critical logic in your application. If the database layer is broken or unreliable, the entire system becomes unstable.

Database testing ensures that:

  • records are inserted correctly
  • fields are updated correctly
  • records are deleted safely
  • constraints work as expected
  • relationships behave correctly
  • queries return the expected results
  • migrations create appropriate schemas
  • validation prevents invalid data from being saved

Without database tests, bugs can silently corrupt data or return inconsistent results.


Laravel’s Built-In Tools for Database Testing

Laravel provides everything needed for database testing inside its testing suite. Tools include:

  • RefreshDatabase
  • DatabaseMigrations
  • DatabaseTransactions
  • model factories
  • in-memory SQLite
  • PHPUnit helpers
  • Pest helpers
  • database assertion methods

Laravel makes database testing easy, fast, and reliable.


Using RefreshDatabase Trait

The most commonly used trait for database testing is:

use RefreshDatabase;

This trait runs all migrations before each test and refreshes the database after each test.

Advantages:

  • ensures a clean state before every test
  • prevents leftover records interfering with tests
  • runs quickly when using SQLite in-memory mode

Example:

class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_is_created()
{
    User::factory()->create(['email' => '[email protected]']);
    $this->assertDatabaseHas('users', [
        'email' => '[email protected]'
    ]);
}
}

Using SQLite In-Memory Database for Faster Testing

Laravel allows using an in-memory database for extremely fast tests.

Set the test environment in phpunit.xml:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

In-memory databases run entirely in RAM:

  • migrations run instantly
  • inserts and queries execute quickly
  • no persistent files are created

This speeds up large test suites dramatically.


Using DatabaseMigrations Trait

Another option is:

use DatabaseMigrations;

This runs the migrations only once per test class before tests start, then rolls them back after.

This can be faster for large databases.


Using DatabaseTransactions Trait

This trait wraps each test case inside a database transaction:

use DatabaseTransactions;

Every test rolls back all database changes automatically.

Advantages:

  • extremely fast
  • no need to re-run migrations

Limitation:

  • does not work with SQLite in-memory databases
  • does not work with multiple database connections

Asserting Database State

Laravel gives powerful testing helpers to inspect database records.

assertDatabaseHas

Checks if a record exists:

$this->assertDatabaseHas('users', [
'email' =&gt; '[email protected]'
]);

Example after creating user:

User::factory()->create(['email' => '[email protected]']);

assertDatabaseMissing

Checks that a record does not exist:

$this->assertDatabaseMissing('users', [
'email' =&gt; '[email protected]'
]);

assertDeleted

Checks that a model was deleted:

$user = User::factory()->create();

$user->delete();

$this->assertSoftDeleted($user);

For hard delete:

$this->assertDatabaseMissing('users', ['id' => $user->id]);

Testing Insert Operations

Database insert operations are frequently tested.

Example:

public function test_user_insertion()
{
User::factory()-&gt;create(&#91;
    'name' =&gt; 'John Doe'
]);
$this-&gt;assertDatabaseHas('users', &#91;
    'name' =&gt; 'John Doe'
]);
}

You can also insert manually:

DB::table('users')->insert([
'name' =&gt; 'John Doe',
'email' =&gt; '[email protected]'
]);

Then assert:

$this->assertDatabaseHas('users', [
'email' =&gt; '[email protected]'
]);

Testing Update Operations

Updating records:

public function test_user_update()
{
$user = User::factory()-&gt;create();
$user-&gt;update(&#91;'name' =&gt; 'Updated Name']);
$this-&gt;assertDatabaseHas('users', &#91;
    'id' =&gt; $user-&gt;id,
    'name' =&gt; 'Updated Name'
]);
}

You should also assert the old value is missing:

$this->assertDatabaseMissing('users', [
'name' =&gt; 'Old Name'
]);

Testing Delete Operations

Test hard delete:

public function test_user_delete()
{
$user = User::factory()-&gt;create();
$user-&gt;delete();
$this-&gt;assertDatabaseMissing('users', &#91;
    'id' =&gt; $user-&gt;id
]);
}

Test soft delete:

$this->assertSoftDeleted($user);

Testing Soft Deletes

Soft deletes keep the record but fill the deleted_at field.

Check soft deletion:

$this->assertSoftDeleted('users', [
'id' =&gt; $user-&gt;id
]);

Restore record:

$user->restore();

Now assert:

$this->assertDatabaseHas('users', [
'id' =&gt; $user-&gt;id
]);

Testing Relationships

Testing relationships ensures Eloquent relations work correctly.

belongsTo relationship

public function test_post_belongs_to_user()
{
$user = User::factory()-&gt;create();
$post = Post::factory()-&gt;create(&#91;'user_id' =&gt; $user-&gt;id]);
$this-&gt;assertTrue($post-&gt;user-&gt;is($user));
}

hasMany relationship

public function test_user_has_many_posts()
{
$user = User::factory()-&gt;create();
Post::factory()-&gt;count(3)-&gt;create(&#91;'user_id' =&gt; $user-&gt;id]);
$this-&gt;assertCount(3, $user-&gt;posts);
}

belongsToMany relationship

public function test_role_assignment()
{
$role = Role::factory()-&gt;create();
$user = User::factory()-&gt;create();
$user-&gt;roles()-&gt;attach($role);
$this-&gt;assertDatabaseHas('role_user', &#91;
    'role_id' =&gt; $role-&gt;id,
    'user_id' =&gt; $user-&gt;id
]);
}

Testing Model Factories

Factories make database tests easier.

Create a factory:

php artisan make:factory UserFactory

Factory example:

public function definition()
{
return &#91;
    'name' =&gt; $this-&gt;faker-&gt;name,
    'email' =&gt; $this-&gt;faker-&gt;unique()-&gt;safeEmail
];
}

Use factory:

User::factory()->count(5)->create();

Factories produce reliable dummy data.


Testing Validation Against Database

Laravel validation often includes database rules.

Example: unique email validation.

Test:

public function test_email_must_be_unique()
{
User::factory()-&gt;create(&#91;'email' =&gt; '[email protected]']);
$response = $this-&gt;post('/register', &#91;
    'email' =&gt; '[email protected]',
    'password' =&gt; 'password'
]);
$response-&gt;assertSessionHasErrors('email');
}

Database-backed validation ensures correctness.


Testing Authentication Against Database

Testing login:

public function test_user_login()
{
$user = User::factory()-&gt;create(&#91;
    'password' =&gt; bcrypt('secret')
]);
$response = $this-&gt;post('/login', &#91;
    'email' =&gt; $user-&gt;email,
    'password' =&gt; 'secret'
]);
$response-&gt;assertRedirect('/home');
$this-&gt;assertAuthenticatedAs($user);
}

Database verifies user record.


Testing API Endpoints With Database

Example API test:

public function test_api_returns_users()
{
User::factory()-&gt;count(3)-&gt;create();
$response = $this-&gt;getJson('/api/users');
$response-&gt;assertStatus(200)
         -&gt;assertJsonCount(3, 'data');
}

Testing ensures correct output.


Testing Pivot Tables

BelongsToMany relationships require pivot assertions.

$this->assertDatabaseHas('role_user', [
'user_id' =&gt; $user-&gt;id,
'role_id' =&gt; $role-&gt;id
]);

Detach:

$user->roles()->detach($role);

Assert missing:

$this->assertDatabaseMissing('role_user', [
'role_id' =&gt; $role-&gt;id
]);

Testing JSON Fields and Casting

Models often include JSON fields.

Example:

public function test_json_field_stored_properly()
{
$product = Product::factory()-&gt;create(&#91;
    'details' =&gt; &#91;'color' =&gt; 'red', 'size' =&gt; 'M']
]);
$this-&gt;assertDatabaseHas('products', &#91;
    'id' =&gt; $product-&gt;id
]);
}

Testing Database Transactions

Test that transaction rolls back:

DB::beginTransaction();

User::factory()->create();

DB::rollBack();

$this->assertDatabaseCount('users', 0);

Testing Seeders

Run seeder inside tests:

$this->seed(UserSeeder::class);

Verify:

$this->assertDatabaseHas('users', [
'email' =&gt; '[email protected]'
]);

Testing Migrations

You can test if columns exist:

$this->assertTrue(
Schema::hasColumn('users', 'email')
);

Testing Performance of Queries

You can inspect queries using DB::enableQueryLog()

DB::enableQueryLog();

User::all();

$log = DB::getQueryLog();

Assertions can verify query count.


Using Pest for Database Testing

Pest offers cleaner syntax.

Example:

uses(RefreshDatabase::class);

it('creates a user', function () {
User::factory()-&gt;create(&#91;'email' =&gt; '[email protected]']);
$this-&gt;assertDatabaseHas('users', &#91;'email' =&gt; '[email protected]']);
});

Pest is expressive and modern.


Best Practices for Database Testing

  • always use RefreshDatabase
  • use SQLite in-memory for speed
  • avoid testing Laravel internals
  • isolate each test
  • use factories for test data
  • write descriptive test names
  • test both success and failure cases
  • test relationships
  • test soft deletes
  • test validation rules
  • test edge cases like null values

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *