> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Mock

> 使用 Laravel 的 Mock / 测试替身功能，将测试作为独立单元来执行的方法。

## 为什么使用 Mock

在编写测试时，有些处理是我们实际上并不希望真正执行的，比如发送邮件、读写缓存、调用外部 API 等。
用「装作真实存在的假对象」替换这些处理，就叫做 Mock（模拟）。

使用 Mock 的好处：

* **测试更快** — 不执行外部服务或耗时处理，测试很快完成
* **测试更稳定** — 不受外部服务状态影响，结果始终一致
* **测试范围更清晰** — 可以只验证某一个类或方法

Laravel 为事件、任务、门面等提供了即用型的 Mock 辅助方法。
内部使用 [Mockery](https://github.com/mockery/mockery)，无需复杂设置即可使用。

```mermaid theme={null}
graph LR
    A["测试"] --> B["Mock<br>（假对象）"]
    A --> C["生产代码"]
    B --> D["不会执行<br>（外部 API、邮件等）"]
    C --> B
    style B fill:#f9a,stroke:#c66
    style D fill:#eee,stroke:#aaa
```

## Mock 对象

要 mock 通过服务容器注入的对象，可以将 mock 实例绑定到容器上。
这样容器就会使用 mock 实例，而不是新建对象。

<CodeGroup>
  ```php Pest theme={null}
  use App\Service;
  use Mockery;
  use Mockery\MockInterface;

  test('something can be mocked', function () {
      $this->instance(
          Service::class,
          Mockery::mock(Service::class, function (MockInterface $mock) {
              $mock->expects('process');
          })
      );
  });
  ```

  ```php PHPUnit theme={null}
  use App\Service;
  use Mockery;
  use Mockery\MockInterface;

  public function test_something_can_be_mocked(): void
  {
      $this->instance(
          Service::class,
          Mockery::mock(Service::class, function (MockInterface $mock) {
              $mock->expects('process');
          })
      );
  }
  ```
</CodeGroup>

### `mock()` 方法

使用 Laravel 测试基类提供的 `mock()` 方法，可以更简洁地写出等效逻辑。

```php theme={null}
use App\Service;
use Mockery\MockInterface;

$mock = $this->mock(Service::class, function (MockInterface $mock) {
    $mock->expects('process');
});
```

### `partialMock()` 方法

如果只想 mock 对象的部分方法，可以使用 `partialMock()`。
未被 mock 的方法会照常执行。

```php theme={null}
use App\Service;
use Mockery\MockInterface;

$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
    $mock->expects('process');
});
```

### `spy()` 方法

Spy（间谍对象）与 mock 类似，不同之处在于它是在代码执行之后再验证交互。
Mock 在事前设置「这个方法应当被调用」，而 spy 是在事后检查「这个方法是否被调用了」。

```php theme={null}
use App\Service;

$spy = $this->spy(Service::class);

// ...执行被测代码...

$spy->shouldHaveReceived('process');
```

```mermaid theme={null}
graph TD
    subgraph "Mock"
        M1["预先设置期望<br>expects('process')"] --> M2["执行代码"] --> M3["自动校验"]
    end
    subgraph "Spy"
        S1["设置 spy"] --> S2["执行代码"] --> S3["事后校验<br>shouldHaveReceived('process')"]
    end
```

## Mock 门面

与普通的静态方法调用不同，[门面](/zh/facades)是可以被 mock 的。
它拥有与依赖注入相当的可测试性，同时保留了简洁的书写方式。

以下面这个使用缓存的控制器为例：

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * 返回应用中的全部用户列表
     */
    public function index(): array
    {
        $value = Cache::get('key');

        return [
            // ...
        ];
    }
}
```

要 mock `Cache` 门面的 `get` 方法，请使用 `expects()`。

<CodeGroup>
  ```php Pest theme={null}
  <?php

  use Illuminate\Support\Facades\Cache;

  test('get index', function () {
      Cache::expects('get')
          ->with('key')
          ->andReturn('value');

      $response = $this->get('/users');

      // ...
  });
  ```

  ```php PHPUnit theme={null}
  <?php

  namespace Tests\Feature;

  use Illuminate\Support\Facades\Cache;
  use Tests\TestCase;

  class UserControllerTest extends TestCase
  {
      public function test_get_index(): void
      {
          Cache::expects('get')
              ->with('key')
              ->andReturn('value');

          $response = $this->get('/users');

          // ...
      }
  }
  ```
</CodeGroup>

<Warning>
  请勿 mock `Request` 门面。作为替代，请将输入值传给 `get`、`post` 等 HTTP 测试方法。同样，请不要 mock `Config` 门面，而是在测试内调用 `Config::set()`。
</Warning>

## 门面 Spy

要用 spy 监视门面，请调用对应门面的 `spy()` 方法。
Spy 适合在代码执行后再验证交互的场景。

<CodeGroup>
  ```php Pest theme={null}
  <?php

  use Illuminate\Support\Facades\Cache;

  test('values are stored in cache', function () {
      Cache::spy();

      $response = $this->get('/');

      $response->assertStatus(200);

      Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
  });
  ```

  ```php PHPUnit theme={null}
  use Illuminate\Support\Facades\Cache;

  public function test_values_are_stored_in_cache(): void
  {
      Cache::spy();

      $response = $this->get('/');

      $response->assertStatus(200);

      Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
  }
  ```
</CodeGroup>

## 操作时间

在测试与时间相关的逻辑时，能够修改 `now()` 或 `Carbon::now()` 返回的时刻非常方便。
Laravel 的功能测试基类提供了操作时间的辅助方法。

### `travel()` — 移动时间

<CodeGroup>
  ```php Pest theme={null}
  test('time can be manipulated', function () {
      // 前进到未来
      $this->travel(5)->milliseconds();
      $this->travel(5)->seconds();
      $this->travel(5)->minutes();
      $this->travel(5)->hours();
      $this->travel(5)->days();
      $this->travel(5)->weeks();
      $this->travel(5)->years();

      // 回到过去
      $this->travel(-5)->hours();

      // 移动到某个具体时刻
      $this->travelTo(now()->subHours(6));

      // 回到当前时刻
      $this->travelBack();
  });
  ```

  ```php PHPUnit theme={null}
  public function test_time_can_be_manipulated(): void
  {
      // 前进到未来
      $this->travel(5)->milliseconds();
      $this->travel(5)->seconds();
      $this->travel(5)->minutes();
      $this->travel(5)->hours();
      $this->travel(5)->days();
      $this->travel(5)->weeks();
      $this->travel(5)->years();

      // 回到过去
      $this->travel(-5)->hours();

      // 移动到某个具体时刻
      $this->travelTo(now()->subHours(6));

      // 回到当前时刻
      $this->travelBack();
  }
  ```
</CodeGroup>

### 使用闭包进行时间移动

向时间移动方法传入闭包时，会在指定时刻将时间冻结并执行闭包，闭包执行完毕后再恢复到原来的时间。

```php theme={null}
$this->travel(5)->days(function () {
    // 在 5 天后的状态下进行测试
});

$this->travelTo(now()->subDays(10), function () {
    // 在特定时刻下进行测试
});
```

### `freezeTime()` — 冻结时间

`freezeTime()` 会冻结当前时间。`freezeSecond()` 则会把时间冻结到当前秒的开头。

```php theme={null}
use Illuminate\Support\Carbon;

// 冻结时间执行闭包，结束后再恢复
$this->freezeTime(function (Carbon $time) {
    // ...
});

// 在当前秒的开头冻结时间并执行闭包
$this->freezeSecond(function (Carbon $time) {
    // ...
});
```

### 实用示例：非活跃线程被锁定

时间操作非常适合测试类似论坛「一段时间无操作后帖子被锁定」这样的功能。

<CodeGroup>
  ```php Pest theme={null}
  use App\Models\Thread;

  test('forum threads lock after one week of inactivity', function () {
      $thread = Thread::factory()->create();

      $this->travel(1)->week();

      expect($thread->isLockedByInactivity())->toBeTrue();
  });
  ```

  ```php PHPUnit theme={null}
  use App\Models\Thread;

  public function test_forum_threads_lock_after_one_week_of_inactivity(): void
  {
      $thread = Thread::factory()->create();

      $this->travel(1)->week();

      $this->assertTrue($thread->isLockedByInactivity());
  }
  ```
</CodeGroup>

<Info>
  `travel()` 等时间操作方法只能在功能测试（继承 `Tests\TestCase` 的类）中使用。在 PHPUnit 的基类 `TestCase` 中无法使用。
</Info>

## 方法一览

### Mock 相关

| 方法                                     | 说明               |
| -------------------------------------- | ---------------- |
| `$this->mock(Class::class, fn)`        | 创建完整 mock 并注册到容器 |
| `$this->partialMock(Class::class, fn)` | 仅 mock 部分方法      |
| `$this->spy(Class::class)`             | 创建 spy 并注册到容器    |
| `$this->instance(Class::class, $mock)` | 将任意 mock 实例注册到容器 |
| `Facade::expects('method')`            | Mock 门面的方法       |
| `Facade::spy()`                        | 用 spy 监视门面       |
| `$spy->shouldHaveReceived('method')`   | 校验 spy 上某方法是否被调用 |

### 时间操作相关

| 方法                         | 说明            |
| -------------------------- | ------------- |
| `$this->travel(n)->unit()` | 按指定单位移动时间     |
| `$this->travelTo(Carbon)`  | 移动到特定时刻       |
| `$this->travelBack()`      | 回到当前时刻        |
| `$this->freezeTime(fn)`    | 冻结时间并执行闭包     |
| `$this->freezeSecond(fn)`  | 在秒开头冻结时间并执行闭包 |


## Related topics

- [测试](/zh/packages/laravel-bluesky/testing.md)
- [使用 Pest 进行进阶测试](/zh/advanced/testing-pest.md)
- [Core —— AT Protocol 核心操作](/zh/packages/laravel-bluesky/core.md)
- [Laravel 9 升级到 10](/zh/blog/upgrade-9-to-10.md)
- [创建 AI SDK 的自定义 Provider](/zh/advanced/ai-sdk-custom-provider.md)
