laravel测试需明确分层:unit测试纯逻辑,继承phpunit\framework\testcase;feature测试全链路,继承tests\testcase;databasemigrations适合多数场景,refreshdatabase用于外键复杂情况;mail::fake()须在发送前调用;api测试419错误主因是csrf,可用withoutmiddleware或配置例外。

直接上手写 Laravel 测试,关键不是“会不会”,而是“在哪写、继承谁、怎么隔离、怎么跑”。错一步,php artisan test 就报 Class "UserTest" not found 或者测试变慢十倍。
tests/Unit 和 tests/Feature 该用哪个?
别凭感觉选。Unit 是真隔离,Feature 是全链路模拟。
-
tests/Unit/:适合测试不依赖 HTTP、数据库、服务容器的类,比如PriceCalculator::applyDiscount()、StringHelper::reverseString();应继承PHPUnit\Framework\TestCase或轻量版Tests\Unit\TestCase,避免加载整个 Laravel 应用 -
tests/Feature/:适合验证控制器、路由、中间件、表单提交、API 响应等,比如注册接口是否返回 201、邮件是否触发;必须继承Tests\TestCase,默认带应用上下文和数据库支持 - 用
php artisan make:test UserValidatorTest --unit自动生成 Unit 目录下的文件;漏掉--unit,它会默认扔进 Feature 目录,还继承重载的基类,导致测试启动慢、断言行为异常
DatabaseMigrations 还是 RefreshDatabase?
两者都清库,但机制和代价完全不同——选错直接影响 CI 耗时和测试稳定性。
-
DatabaseMigrations:只对当前测试类中声明了use DatabaseMigrations;的迁移执行 rollback + migrate;快,适合多数场景;但不重放DB::table()->insert()类型的种子数据 -
RefreshDatabase:每次测试前执行php artisan migrate:fresh,重建全部表结构;慢,尤其在表多、外键复杂时;但能确保视图、索引、约束完全一致 - 常见坑:
setUp()里手动DB::table('users')->truncate()+RefreshDatabase→ 清两次库,可能触发外键错误;推荐起步用DatabaseMigrations,等出现SQLSTATE[HY000]: General error: 1005(外键失败)再切过去
Mail::fake() 必须在发邮件前调用
不是“写了就生效”,而是“调用时机决定是否被捕获”。
- 漏掉
Mail::fake():调用Mail::to('x')->send(new WelcomeMail)后,Mail::assertSent(WelcomeMail::class)永远失败 - 顺序写反:把
Mail::fake()放在$this->postJson('/register')之后 → 请求已发出,邮件已尝试发送,fake 来不及拦截 - 想断言收件人或内容:用闭包形式
Mail::assertSent(WelcomeMail::class, function ($mail) { return $mail->hasTo('test@example.com') && $mail->subject === 'Welcome!'; }) -
Mail::fake()会自动禁用真实 SMTP 配置,不用改MAIL_MAILER环境变量
API 测试返回 419?先看 CSRF 中间件
Laravel 8+ 默认启用 VerifyCsrfToken,而 $this->postJson() 不带 token,这是最常被忽略的 HTTP 测试卡点。
- 最快解法:在测试类顶部加
use WithoutMiddleware;,跳过所有中间件(包括 CSRF、Auth、Throttle) - 更精准做法:在
phpunit.xml中为测试环境配置SESSION_DRIVER=database并在app/Http/Middleware/VerifyCsrfToken.php的$except数组里加 API 路由,如'api/*' - 注意:
WithoutMiddleware是全局跳过,若测试本身要验证中间件逻辑(比如权限拦截),就不能用它
真正难的不是写断言,而是判断哪一层该测、哪一层该 mock、哪一层该跳过。比如测试一个带邮件通知的订单创建流程,你得明确:模型逻辑走 Unit,HTTP 流程走 Feature,邮件发不发用 fake,第三方支付回调则必须用 Http::fake() 拦截——混在一起写,迟早失控。











