> ## 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 與 test double 功能，讓測試以獨立單位執行

## 為何要使用 Mock

在撰寫測試時，會有一些實際上不希望執行的處理，例如寄信、快取讀寫或呼叫外部 API。
把這類處理替換為「假裝的替身」的作法就稱為 mock。

Mock 的好處包含：

* **測試速度快**：不執行外部服務或耗時處理，測試很快完成
* **測試穩定**：不受外部服務狀態影響，結果始終一致
* **測試範圍清晰**：可以只驗證單一類別或方法

Laravel 已提供可立即使用的 mock 輔助函式，涵蓋事件、Job、Facade 等。
其內部使用 [Mockery](https://github.com/mockery/mockery)，不需複雜設定即可使用。

```mermaid theme={null}
graph LR
    A["測試"] --> B["Mock<br>(假物件)"]
    A --> C["正式程式碼"]
    B --> D["不會執行<br>(外部 API、Email 等)"]
    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
```

## Facade 的 Mock

[Facade](/zh-TW/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` Facade 的 `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` Facade。改為在 `get` 或 `post` 等 HTTP 測試方法上傳入輸入資料。同樣地，別 mock `Config` Facade，改在測試中直接呼叫 `Config::set()`。
</Warning>

## Facade Spy

要以 spy 監控 Facade，可呼叫對應 Facade 的 `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 的 feature test 基底類別提供了操作時間的輔助方法。

### `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()` 等時間操作方法只能在 feature 測試（繼承 `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 的方法   |
| `Facade::spy()`                        | 以 spy 監控 Facade   |
| `$spy->shouldHaveReceived('method')`   | 對 spy 驗證方法是否被呼叫   |

### 時間操作相關

| 方法                         | 說明           |
| -------------------------- | ------------ |
| `$this->travel(n)->unit()` | 依指定單位移動時間    |
| `$this->travelTo(Carbon)`  | 前往特定時間       |
| `$this->travelBack()`      | 回到目前時間       |
| `$this->freezeTime(fn)`    | 凍結時間執行閉包     |
| `$this->freezeSecond(fn)`  | 於秒起點凍結時間執行閉包 |


## Related topics

- [測試](/zh-TW/packages/laravel-bluesky/testing.md)
- [使用 Pest 進行進階測試](/zh-TW/advanced/testing-pest.md)
- [Core — AT Protocol 核心操作](/zh-TW/packages/laravel-bluesky/core.md)
- [從 Laravel 9 升級到 10](/zh-TW/blog/upgrade-9-to-10.md)
- [建立 AI SDK 的自定義 Provider](/zh-TW/advanced/ai-sdk-custom-provider.md)
