CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Laravel 实战入门/课时/测试
RawBlame
课时目录12 节

Laravel 实战入门

  1. 01Laravel 概览
  2. 02目录结构
  3. 03路由
  4. 04控制器
  5. 05Blade 模板
  6. 06Eloquent ORM
  7. 07认证与授权
  8. 08验证与异常
  9. 09队列与事件
  10. 10运维与工具
  11. 11测试
  12. 12综合练习

测试

PHPUnit 与 Pest。

30 分钟

测试

这一课你将能做到

  1. 用 php artisan test 运行 Unit/Feature 测试,读懂 PHPUnit 类风格与 Pest 函数风格两种用例
  2. 用 RefreshDatabase+Factory 在隔离数据库中快速搭建任意场景
  3. 用框架 Fake 与 Mockery 模拟邮件、队列、事件、外部 HTTP,让测试快速确定

为什么要学

没有测试,每次改动都是裸奔:无法确认新功能没破坏老逻辑,重构只能手点页面。测试把「我以为能跑」变成「可重复证明能跑」,并在每次提交时自动执行。Laravel 测试底层是 PHPUnit(JUnit 的 PHP 同源思想),可选语法更轻快的 Pest,且每个子系统都有替身:不必真发邮件、真扣款。对应 Spring 的 JUnit 5+Mockito+MockMvc+@SpringBootTest。

先建立图像

   ▲ 慢/少
 ┌──────┐
 │ Unit │   纯逻辑:判分、价格计算
 ├──────┤
 │Feature│  HTTP→路由→控制器→数据库(主力)
 ├──────┤
 │ E2E  │   Dusk 操作真实浏览器(少量)
 └──────┘
   ▼ 快/多
LaravelSpring Boot
tests/Feature@SpringBootTest+MockMvc
tests/UnitJUnit+Mockito
FactoryTest Fixture
RefreshDatabase事务回滚/H2
Http::fake/Mail::fakeMockito/WireMock

跟着做

步骤 1:环境与第一个测试

phpunit.xml 关键项:

<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="QUEUE_CONNECTION" value="sync"/>
php artisan make:test OrderPayTest
php artisan make:test PriceCalculatorTest --unit
php artisan test --filter=OrderPayTest --stop-on-failure

步骤 2:写 Feature 测试

tests/Feature/OrderPayTest.php:

<?php

namespace Tests\Feature;

use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class OrderPayTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_sees_their_paid_order(): void
    {
        $user  = User::factory()->create();
        $order = Order::factory()->create(['user_id' => $user->id, 'status' => 'paid']);

        $this->actingAs($user)->getJson("/api/orders/{$order->id}")
            ->assertOk()
            ->assertJsonPath('data.status', 'paid')
            ->assertJsonStructure(['data' => ['id', 'order_no', 'amount', 'status']]);

        $this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => 'paid']);
    }

    public function test_user_cannot_see_others_order(): void
    {
        $order = Order::factory()->create();

        $this->actingAs(User::factory()->create())
            ->getJson("/api/orders/{$order->id}")
            ->assertForbidden();
    }
}

常用断言:assertOk/assertNotFound/assertUnauthorized、assertJson、assertSee(Blade)、assertRedirect、assertValidationErrors。

步骤 3:用工厂造数

database/factories/OrderFactory.php:

class OrderFactory extends Factory
{
    protected $model = Order::class;

    public function definition(): array
    {
        return [
            'order_no' => 'NO'.$this->faker->unique()->numerify('##########'),
            'amount'   => $this->faker->numberBetween(100, 99900),
            'status'   => 'pending',
            'channel'  => 'mock',
        ];
    }

    public function paid(): static
    {
        return $this->state(fn () => ['status' => 'paid', 'paid_at' => now()]);
    }
}
Order::factory()->paid()->count(10)->create(['user_id' => $user->id]);
$user = User::factory()->has(Order::factory()->count(3)->paid())->create();
Order::factory()->make();   // make 只生成不入库,create 入库

RefreshDatabase 原理:首次在内存库跑全部迁移,之后每个用例包事务自动回滚。迁移必须兼容 SQLite;用了 MySQL 专有功能时 CI 改用 Testcontainers 起真实 MySQL。

步骤 4:Pest 风格

Starter Kit 新项目可能默认 Pest,同样的用例写成:

use function Pest\Laravel\{actingAs, getJson};

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

it('lets a user view their paid order', function () {
    $order = Order::factory()->paid()->create();
    actingAs($order->user)->getJson("/api/orders/{$order->id}")->assertOk();
});

expect(1 + 1)->toBe(2);
expect($order->amount)->toBe(19900);

it 是测试函数、expect 是断言,与 PHPUnit 用例可共存,底层仍是 PHPUnit。

步骤 5:Fake 与 Mock

框架 Fake 一行替换所有调用:

use Illuminate\Support\Facades\{Http, Mail, Queue, Event, Storage};

Http::fake(['api.sms.com/*' => Http::response(['ok' => true], 200)]);
Mail::fake();   // ...触发逻辑...
Mail::assertSent(OrderReceipt::class);
Queue::assertPushed(\App\Jobs\SendOrderReceipt::class);
Event::fake([\App\Events\OrderPaid::class]);
Event::assertDispatched(\App\Events\OrderPaid::class);
Storage::fake('s3');

控制自己的服务用 Mockery(等价 Mockito):

$this->mock(\App\Contracts\PayGateway::class, function ($mock) {
    $mock->shouldReceive('charge')->once()->with(19900)->andReturn(['trade_no' => 'T-1']);
});

$this->postJson('/api/orders', ['amount' => 19900])->assertOk();

替身经容器注入才生效——代码里若 new SomeClient() 则无法替换,这是依赖注入的又一个理由。

步骤 6:覆盖率与 CI

php artisan test --coverage            # 需 Xdebug/PCOV
php artisan test --coverage --min=80   # 覆盖率门禁
php artisan test --parallel

CI 流程固定:composer install → 准备数据库 → php artisan test,失败即阻断合并。

讲清楚要点

  1. Feature 优先:多数 bug 在「胶水层」(路由、验证、权限、序列化),只有 Feature 测得到;纯计算下沉 Unit。
  2. 一个用例只验一件事:方法名用行为句描述,场景复杂拆多个方法。
  3. Factory 是默认世界:用例只写相关字段,其余工厂补齐,抗 schema 变化。
  4. 不 Mock 你不拥有的东西:优先 Fake 门面与自有接口;第三方打在 Http::fake 协议层。
  5. 测试要快:内存库、少 seed、并行;慢测试会被团队跳过。
  6. 测试配置与生产尽量像:关键路径在 CI 用真实 MySQL、真实队列验证。

你可能会懵的地方

  • Unit 和 Feature 怎么分? 碰数据库、认证、HTTP 的放 Feature;new 个类直接调的放 Unit。拿不准先写 Feature。
  • RefreshDatabase 会清开发库吗? 不会,只作用于测试连接;但千万别把测试连接指向开发库。
  • assertJson 明明对却失败? 子集匹配但数组顺序、类型("1" vs 1)要一致;精确断言用 assertJsonPath。
  • Mock 了但没用到? 代码必须经容器/构造注入拿依赖;直接 new、静态调用都绕开替换。
  • Pest 要专门学吗? 阅读门槛很低,写惯 PHPUnit 也够用,二者团队统一即可。

错法对照

错法问题正解
用例间靠执行顺序共享数据顺序一变全挂RefreshDatabase+工厂
测试真调支付短信慢贵且脏Http::fake/Mock
未认证却断言 200实际 401actingAs
一个测试断言十几件事失败难定位拆单用例
Mock 第三方 SDK 内部方法SDK 升级假通过Http::fake 打协议层
代码里 new 客户端无法替换接口+容器绑定
只断言 200业务错照上线断言内容/落库/副作用

卡点急救

  1. 全部报 table not found:检查 RefreshDatabase 是否引入、新迁移有无语法错误。
  2. 找不到 Factory:自定义模型目录时用 newFactory() 指定;确认模型 use HasFactory。
  3. CI 与本地结果不一致:多为 SQLite/MySQL 差异(JSON、排序、布尔),CI 加 MySQL 矩阵。

面试常问

  • 项目里怎么写测试? Feature 为主:RefreshDatabase 隔离、工厂造场景,断言覆盖状态码、响应与落库,Mail/Queue/Event fake 与 Http::fake 隔离副作用;CI 全量+覆盖率门禁。答出「隔离、替身、断言副作用」即合格。

  • Mock 和 Fake 的区别? Mock 按期望验证交互(次数、参数);Fake 是可用替身(Mail::fake 存集合供断言),成本更低。能 Fake 优先 Fake,自有接口才 Mock。

  • 如何保证用例互不影响? 事务回滚+内存库、phpunit.xml 固定环境、不依赖顺序、Storage::fake;时间逻辑用 $this->travel(5)->days()。

自测三问

  1. 验证「下单后发邮件、订单落库、触发积分事件」,你用哪种测试、哪些 Fake、什么断言?
  2. Service 构造注入 PayGateway,如何让它返回固定结果?代码写成 new PayGateway() 会怎样?
  3. 套件要跑 10 分钟,从哪几方面优化?

小结一句话

Feature 守住主干、工厂与 RefreshDatabase 隔离场景、Fake 与 Mock 切断依赖,测试又快又可信。

延伸

  • 测试入门
  • HTTP 测试
  • 数据库测试
  • Mockery / Pest
  • Spring Boot 测试对照
  • 下一课:综合练习——用「做题平台后端」串起路由、验证、Eloquent、队列、事件与测试。
加载本节练习…
← 上一课运维与工具下一课 →综合练习