HTTP 测试与数据库策略
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read191 words

HTTP 测试与数据库策略

Laravel 的 HTTP 测试让你直接模拟 API 请求而不需要真实的 HTTP 服务器。数据库策略(RefreshDatabase vs DatabaseTransactions)的选择影响测试速度——理解两者的区别才能在速度和可靠性之间找到平衡。


数据库测试策略

RefreshDatabase:每个测试后重建数据库

// tests/Feature/TaskTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
// 行为:
// - 在第一个测试前运行所有 Migration
// - 每个测试后 TRUNCATE 所有表(模拟全新状态)
// - 速度:较慢(每个测试文件重置一次,不是每个测试)
// ✅ 适合:
// - 需要干净数据库状态的测试
// - 测试 Migration 或 Seeder 逻辑
// - 并发测试(每个 Worker 独立数据库)

DatabaseTransactions:事务包裹,自动回滚

use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
// 行为:
// - 每个测试开始前 BEGIN 事务
// - 测试结束后 ROLLBACK(数据自动撤销)
// - 速度:快(不需要重建数据库)
// ❌ 限制:
// - 不能测试需要多个数据库连接的场景
// - 不能测试使用独立事务的代码(如 Queue Jobs 的数据库写入)

推荐策略

// tests/Pest.php
// Feature 测试用 RefreshDatabase(可靠性优先)
uses(RefreshDatabase::class)->in('Feature');
// .env.testing 配置独立测试数据库
DB_DATABASE=taskflow_test
CACHE_STORE=array
QUEUE_CONNECTION=sync
MAIL_MAILER=array

HTTP 测试断言速查

// 响应状态码
$response->assertOk();           // 200
$response->assertCreated();      // 201
$response->assertNoContent();    // 204
$response->assertUnauthorized(); // 401
$response->assertForbidden();    // 403
$response->assertNotFound();     // 404
$response->assertUnprocessable();// 422(验证失败)
$response->assertServerError();  // 500
// JSON 断言
$response->assertJson(['data' => ['title' => '任务标题']]);
$response->assertJsonPath('data.0.title', '第一个任务');
$response->assertJsonCount(5, 'data');
$response->assertJsonStructure(['data' => [['id', 'title', 'status']]]);
$response->assertJsonMissingPath('data.password');  // 确保字段不存在
$response->assertJsonValidationErrors(['title', 'status']);  // 验证错误
// 数据库断言
$this->assertDatabaseHas('tasks', ['title' => '写文档', 'status' => 'pending']);
$this->assertDatabaseMissing('tasks', ['title' => '已删除的任务']);
$this->assertDatabaseCount('tasks', 5);
$this->assertSoftDeleted('tasks', ['id' => 42]);
// 响应头
$response->assertHeader('Content-Type', 'application/json');

测试认证流程

describe('Authentication', function () {
it('can register a new user', function () {
$this->postJson('/api/register', [
'name'                  => 'Test User',
'email'                 => 'test@example.com',
'password'              => 'password',
'password_confirmation' => 'password',
])
->assertCreated()
->assertJsonStructure(['token', 'user' => ['id', 'name', 'email']]);
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
});
it('can login with valid credentials', function () {
$user = User::factory()->create(['password' => bcrypt('secret')]);
$this->postJson('/api/login', [
'email'    => $user->email,
'password' => 'secret',
])
->assertOk()
->assertJsonPath('user.id', $user->id)
->assertJsonStructure(['token']);
});
it('rejects invalid credentials', function () {
$user = User::factory()->create();
$this->postJson('/api/login', [
'email'    => $user->email,
'password' => 'wrong-password',
])->assertUnauthorized();
});
it('can logout and revoke token', function () {
$user = User::factory()->create();
$token = $user->createToken('test')->plainTextToken;
$this->withToken($token)
->postJson('/api/logout')
->assertNoContent();
// Token 被撤销,再次请求应该 401
$this->withToken($token)
->getJson('/api/tasks')
->assertUnauthorized();
});
});

测试权限和授权

describe('Task Authorization', function () {
it('prevents user from updating another users task', function () {
$owner   = User::factory()->create();
$other   = User::factory()->create();
$task    = Task::factory()->forUser($owner)->create();
$this->actingAs($other, 'sanctum')
->putJson("/api/v1/tasks/{$task->id}", ['title' => '篡改标题'])
->assertForbidden();
// 确认数据没有被修改
$this->assertDatabaseMissing('tasks', [
'id'    => $task->id,
'title' => '篡改标题',
]);
});
it('allows admin to access any task', function () {
$admin = User::factory()->admin()->create();
$task  = Task::factory()->create();  // 其他用户的任务
$this->actingAs($admin, 'sanctum')
->getJson("/api/v1/tasks/{$task->id}")
->assertOk();
});
});

测试文件上传

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
it('can upload task attachment', function () {
Storage::fake('s3');  // 使用假 S3,不真实上传
$user = User::factory()->create();
$task = Task::factory()->forUser($user)->create();
$file = UploadedFile::fake()->create('document.pdf', 1024, 'application/pdf');
$this->actingAs($user, 'sanctum')
->postJson("/api/v1/tasks/{$task->id}/attachments", [
'file' => $file,
])
->assertCreated()
->assertJsonStructure(['data' => ['id', 'filename', 'url']]);
// 验证文件已存储
Storage::disk('s3')->assertExists("tasks/{$task->id}/{$file->hashName()}");
});
it('rejects oversized files', function () {
$user = User::factory()->create();
$task = Task::factory()->forUser($user)->create();
// 创建 11MB 的文件(超过 10MB 限制)
$file = UploadedFile::fake()->create('large.pdf', 11 * 1024, 'application/pdf');
$this->actingAs($user, 'sanctum')
->postJson("/api/v1/tasks/{$task->id}/attachments", ['file' => $file])
->assertUnprocessable()
->assertJsonValidationErrors(['file']);
});

下一节Mock 外部服务与队列测试——测试发邮件、支付 API、队列 Job——不应该真实调用外部服务,Mock 和 Fake 让测试快速且可靠。