0

Database testing — RefreshDatabase vs DatabaseTransactions

Intermediate5 min read·lv-26-004
sqlinterview

Concept

Mocking in Laravel tests replaces real implementations with fakes to isolate the code under test and verify interactions. Laravel provides Facade fakes for most built-in services — they're simpler and less error-prone than Mockery.

Facade fakes: Mail::fake(), Event::fake(), Queue::fake(), Storage::fake(), Notification::fake(), Bus::fake(), Http::fake(). Call at the start of the test. The real implementation is replaced. Fake assertions verify the right things were dispatched.

Mail::fake():

  • Mail::assertSent(WelcomeMail::class).
  • Mail::assertSent(WelcomeMail::class, 2) — sent exactly twice.
  • Mail::assertSent(WelcomeMail::class, fn($mail) => $mail->hasTo('alice@example.com')).
  • Mail::assertNotSent(ResetEmail::class).
  • Mail::assertNothingSent().
  • Mail::assertQueued(WelcomeMail::class) — for queueable mailables.

Event::fake():

  • Event::assertDispatched(UserRegistered::class).
  • Event::assertNotDispatched(UserBanned::class).
  • Event::assertDispatchedTimes(OrderPlaced::class, 3).

Queue::fake():

  • Queue::assertPushed(SendWeeklyReport::class).
  • Queue::assertPushedOn('emails', SendWeeklyReport::class).
  • Queue::assertNothingPushed().

Storage::fake(): Creates an in-memory disk. File operations work but nothing touches the real filesystem.

Mockery: For mocking non-facade classes or when you need to assert specific method calls with specific arguments. Use Mockery::mock(MyService::class) and bind it in the container: $this->app->instance(MyService::class, $mock).

Code Example

php
<?php
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;

class RegistrationTest extends TestCase
{
    use RefreshDatabase;

    public function test_registration_sends_welcome_email(): void
    {
        Mail::fake();

        $this->postJson('/api/auth/register', [
            'name'                  => 'Alice',
            'email'                 => 'alice@example.com',
            'password'              => 'password',
            'password_confirmation' => 'password',
        ])->assertCreated();

        Mail::assertSent(\App\Mail\WelcomeMail::class, function($mail) {
            return $mail->hasTo('alice@example.com');
        });
    }

    public function test_registration_fires_user_registered_event(): void
    {
        Event::fake();

        User::factory()->create(['email' => 'alice@example.com']);

        Event::assertDispatched(\App\Events\UserRegistered::class, function($event) {
            return $event->user->email === 'alice@example.com';
        });
    }

    public function test_upload_stores_in_correct_disk(): void
    {
        Storage::fake('s3');
        $user = User::factory()->create();

        $file = UploadedFile::fake()->image('avatar.jpg', 200, 200);

        $this->actingAs($user)
             ->postJson('/api/avatar', ['avatar' => $file])
             ->assertOk();

        Storage::disk('s3')->assertExists('avatars/' . $user->id . '.jpg');
    }

    public function test_order_queues_confirmation_job(): void
    {
        Queue::fake();
        $user = User::factory()->create();

        $this->actingAs($user)->postJson('/api/orders', ['product_id' => 1]);

        Queue::assertPushed(\App\Jobs\SendOrderConfirmation::class, function($job) use ($user) {
            return $job->userId === $user->id;
        });
        Queue::assertPushedOn('emails', \App\Jobs\SendOrderConfirmation::class);
    }
}