Laravel 功能测试与模拟

功能测试(Feature Test)用于验证应用的整体行为,从用户角度模拟请求,确保各个组件协同工作。在 Laravel 中,你可以轻松编写 HTTP 测试、数据库测试,并通过模拟(Mocking)外部依赖来隔离测试范围。本章将系统讲解如何编写功能测试,以及如何利用 Laravel 的辅助方法模拟邮件、事件、队列、HTTP 请求等,构建高效、可靠的测试套件。

🧪 核心理念: 功能测试关注“应用做了什么”,而非“如何实现”。通过模拟外部依赖,你可以专注于测试自己的业务逻辑,避免因第三方服务不可用导致测试失败。

1. 功能测试基础

功能测试类位于 tests/Feature 目录,通常继承自 Tests\TestCase。运行测试:


php artisan test
# 或运行特定测试文件
php artisan test --filter=UserRegistrationTest
                    

一个简单的 HTTP 测试


<?php

namespace Tests\Feature;

use Tests\TestCase;

class HomepageTest extends TestCase
{
    public function test_homepage_returns_ok()
    {
        $response = $this->get('/');
        $response->assertStatus(200);
        $response->assertSee('Welcome');
    }
}
                    

2. HTTP 测试常用断言

断言方法说明
$response->assertStatus(200)断言状态码
$response->assertOk()断言 200 OK
$response->assertViewIs('welcome')断言返回的视图名称
$response->assertViewHas('name', $value)断言视图数据包含指定键值
$response->assertSee('文本')断言响应包含文本
$response->assertJson(['key' => 'value'])断言 JSON 响应包含指定结构
$response->assertRedirect('/login')断言重定向到指定 URL
$response->assertSessionHas('success')断言 Session 包含指定值
$response->assertSessionHasErrors(['email'])断言 Session 包含字段错误

3. 数据库测试

使用 RefreshDatabase trait 可以在每个测试后重置数据库,避免数据污染。


use Illuminate\Foundation\Testing\RefreshDatabase;

class UserRegistrationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_register()
    {
        $response = $this->post('/register', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertDatabaseHas('users', ['email' => 'john@example.com']);
    }
}
                    

模型工厂(Model Factories)

工厂用于快速生成测试数据。Laravel 11 默认使用 database/factories 目录,你可以定义工厂并创建模型:


// 创建单个用户
$user = User::factory()->create();

// 创建带关联的用户
$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();
                    

4. 模拟外部依赖

在功能测试中,我们经常需要模拟邮件、事件、队列、HTTP 请求等,避免实际执行外部操作。Laravel 提供了便捷的 fake()

模拟邮件


use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeMail;

public function test_welcome_email_is_sent()
{
    Mail::fake();

    $this->post('/register', [...]);

    Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
        return $mail->user->id === $user->id;
    });
}
                    

模拟事件


use Illuminate\Support\Facades\Event;
use App\Events\UserRegistered;

Event::fake();

// 执行注册...
Event::assertDispatched(UserRegistered::class);
Event::assertNotDispatched(AnotherEvent::class);
                    

模拟队列


use Illuminate\Support\Facades\Queue;
use App\Jobs\SendWelcomeEmail;

Queue::fake();

// 执行注册...
Queue::assertPushed(SendWelcomeEmail::class, function ($job) use ($user) {
    return $job->user->id === $user->id;
});
                    

模拟 HTTP 请求


use Illuminate\Support\Facades\Http;

Http::fake([
    'api.github.com/*' => Http::response(['name' => 'John'], 200),
]);

$response = $this->get('/github-user');
// 断言响应包含 'John'
                    

模拟通知


use Illuminate\Support\Facades\Notification;
use App\Notifications\UserRegisteredNotification;

Notification::fake();

// 执行注册...
Notification::assertSentTo($user, UserRegisteredNotification::class);
                    

模拟文件存储


use Illuminate\Support\Facades\Storage;

Storage::fake('public');

// 执行文件上传...
Storage::disk('public')->assertExists('uploads/file.jpg');
                    

5. 模拟认证用户

使用 actingAs() 方法模拟已登录用户:


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

$this->actingAs($user)
     ->get('/dashboard')
     ->assertOk();
                    

如果需要模拟特定 guard 的用户,可以传递第二个参数:


$this->actingAs($user, 'api');
                    

6. 测试 JSON API

Laravel 提供了方便的方法测试 JSON API,并验证 JSON 结构。


public function test_api_returns_users()
{
    $user = User::factory()->create();

    $response = $this->getJson('/api/users/' . $user->id);

    $response->assertStatus(200)
             ->assertJson([
                 'id' => $user->id,
                 'name' => $user->name,
             ])
             ->assertJsonStructure([
                 'id', 'name', 'email', 'created_at'
             ]);
}
                    

7. 测试表单验证


public function test_registration_requires_valid_email()
{
    $response = $this->post('/register', [
        'name' => 'John',
        'email' => 'invalid-email',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertSessionHasErrors(['email']);
}
                    

8. 测试文件上传


public function test_user_can_upload_avatar()
{
    Storage::fake('public');

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

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

    Storage::disk('public')->assertExists('avatars/' . $file->hashName());
}
                    

9. 最佳实践

  • 保持测试独立: 每个测试应该独立运行,不依赖其他测试的状态。使用 RefreshDatabaseDatabaseMigrations 确保数据库干净。
  • 模拟外部依赖: 对外部服务(邮件、HTTP、队列)始终使用 fake,避免真实调用。
  • 测试命名规范: 测试方法名应清晰描述测试场景,如 test_user_cannot_register_with_invalid_email
  • 使用工厂生成数据: 利用模型工厂快速创建测试所需的数据,避免硬编码。
  • 测试覆盖关键路径: 确保测试覆盖正常流程和异常流程,如验证失败、权限不足等。
  • 避免过度模拟: 如果模拟过多,测试可能失去意义。保持模拟的范围尽可能小,重点测试自己的代码。
💡 提示: 运行测试时使用 php artisan test --coverage 可查看代码覆盖率(需要安装 Xdebug 或 Pcov)。

10. 总结

Laravel 的功能测试与模拟工具让编写高质量测试变得简单而愉快。通过模拟外部依赖,你可以快速验证业务逻辑,确保应用在真实环境中正确运行。养成编写测试的习惯,将极大提升代码的可维护性和自信心。

📖 官方文档: HTTP TestsMocking 提供了更全面的指南。