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

# 測試入門

> 使用 Pest 與 PHPUnit 撰寫 Laravel 應用測試的方法

## 什麼是測試

測試是自動驗證程式碼是否如預期運作的機制。
撰寫測試後，加入功能或修改時能迅速確認既有行為是否被破壞。
在團隊開發中，有測試就能安心變更程式碼與 review。

Laravel 從一開始就內建測試支援，可使用 [Pest](https://pestphp.com) 與 [PHPUnit](https://phpunit.de) 兩者。
全新安裝時已自動準備 `phpunit.xml` 設定檔與 `tests/` 目錄。

<Tip>
  Pest 建構於 PHPUnit 之上，能以更簡潔易讀的語法撰寫測試。初次開始寫測試建議選 Pest。
</Tip>

## `tests/` 目錄結構

```
tests/
├── Feature/      # 功能測試
│   └── ExampleTest.php
├── Unit/         # 單元測試
│   └── ExampleTest.php
└── TestCase.php
```

* **`Feature/`** — 放功能測試。用於包含 HTTP 請求等較大單位的測試。適合驗證多個物件協作、API 端點等接近整體應用的測試。**多數測試都會放這裡。**
* **`Unit/`** — 放單元測試。用於單一類別或方法等小單位的測試。不會啟動 Laravel 應用，因此不能使用資料庫或其他框架功能。

## 建立測試

以 `make:test` Artisan 指令產生新的測試類別。

```shell theme={null}
# 建立 Feature 測試（預設）
php artisan make:test TodoTest

# 建立 Unit 測試
php artisan make:test TodoTest --unit
```

會產生 `tests/Feature/TodoTest.php`。

## 執行測試

以 `php artisan test` 指令執行測試。

```shell theme={null}
php artisan test
```

直接執行 `vendor/bin/pest` 或 `vendor/bin/phpunit` 也能得到相同結果，但 `php artisan test` 有較易讀的輸出，較推薦。

要只執行特定測試套組時可用選項：

```shell theme={null}
# 只執行 Feature 測試
php artisan test --testsuite=Feature

# 失敗即停止
php artisan test --stop-on-failure
```

## 基本測試撰寫

簡單斷言的範例。

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

  test('true 為 true', function () {
      expect(true)->toBeTrue();
  });

  test('確認字串是否包含', function () {
      $message = 'Hello, Laravel!';

      expect($message)->toContain('Laravel');
  });
  ```

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

  namespace Tests\Unit;

  use PHPUnit\Framework\TestCase;

  class ExampleTest extends TestCase
  {
      public function test_true_is_true(): void
      {
          $this->assertTrue(true);
      }

      public function test_string_contains_laravel(): void
      {
          $message = 'Hello, Laravel!';

          $this->assertStringContainsString('Laravel', $message);
      }
  }
  ```
</CodeGroup>

### 常用斷言

| Pest                          | PHPUnit                                     | 說明        |
| ----------------------------- | ------------------------------------------- | --------- |
| `expect($x)->toBe($y)`        | `$this->assertSame($y, $x)`                 | 嚴格相等      |
| `expect($x)->toEqual($y)`     | `$this->assertEquals($y, $x)`               | 相等（不論型別）  |
| `expect($x)->toBeTrue()`      | `$this->assertTrue($x)`                     | 為 `true`  |
| `expect($x)->toBeFalse()`     | `$this->assertFalse($x)`                    | 為 `false` |
| `expect($x)->toBeNull()`      | `$this->assertNull($x)`                     | 為 `null`  |
| `expect($x)->toContain($y)`   | `$this->assertStringContainsString($y, $x)` | 包含字串      |
| `expect($x)->toHaveCount($n)` | `$this->assertCount($n, $x)`                | 元素數量一致    |

## HTTP 測試

Laravel 的 HTTP 測試無需實際啟動 HTTP 伺服器即可模擬對路由的請求。
這是 `Feature/` 目錄下測試類別中可用的強大功能。

### 確認頁面顯示

用 `get()` 方法送 GET 請求並驗證回應。

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

  test('首頁可顯示', function () {
      $response = $this->get('/');

      $response->assertStatus(200);
  });
  ```

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

  namespace Tests\Feature;

  use Tests\TestCase;

  class ExampleTest extends TestCase
  {
      public function test_top_page_is_displayed(): void
      {
          $response = $this->get('/');

          $response->assertStatus(200);
      }
  }
  ```
</CodeGroup>

### ToDo 應用的 HTTP 測試範例

以有 `Todo` model 的應用為例，測試 CRUD 各項操作。

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

  use App\Models\Todo;
  use Illuminate\Foundation\Testing\RefreshDatabase;

  uses(RefreshDatabase::class);

  test('ToDo 列表頁面可顯示', function () {
      Todo::factory()->count(3)->create();

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

      $response->assertStatus(200);
      $response->assertSee('列表');
  });

  test('可以新增 ToDo', function () {
      $response = $this->post('/todos', [
          'title' => '學 Laravel',
      ]);

      $response->assertRedirect('/todos');
      $this->assertDatabaseHas('todos', ['title' => '學 Laravel']);
  });

  test('可以刪除 ToDo', function () {
      $todo = Todo::factory()->create();

      $response = $this->delete("/todos/{$todo->id}");

      $response->assertRedirect('/todos');
      $this->assertDatabaseMissing('todos', ['id' => $todo->id]);
  });
  ```

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

  namespace Tests\Feature;

  use App\Models\Todo;
  use Illuminate\Foundation\Testing\RefreshDatabase;
  use Tests\TestCase;

  class TodoTest extends TestCase
  {
      use RefreshDatabase;

      public function test_todo_list_is_displayed(): void
      {
          Todo::factory()->count(3)->create();

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

          $response->assertStatus(200);
          $response->assertSee('列表');
      }

      public function test_todo_can_be_created(): void
      {
          $response = $this->post('/todos', [
              'title' => '學 Laravel',
          ]);

          $response->assertRedirect('/todos');
          $this->assertDatabaseHas('todos', ['title' => '學 Laravel']);
      }

      public function test_todo_can_be_deleted(): void
      {
          $todo = Todo::factory()->create();

          $response = $this->delete("/todos/{$todo->id}");

          $response->assertRedirect('/todos');
          $this->assertDatabaseMissing('todos', ['id' => $todo->id]);
      }
  }
  ```
</CodeGroup>

### 常用回應斷言

| 方法                                      | 說明             |
| --------------------------------------- | -------------- |
| `assertStatus(200)`                     | 確認回應的 HTTP 狀態碼 |
| `assertOk()`                            | 確認狀態碼為 200     |
| `assertRedirect('/path')`               | 確認導向指定 URL     |
| `assertSee('文字')`                       | 確認回應本體是否包含文字   |
| `assertDontSee('文字')`                   | 確認回應本體不包含文字    |
| `assertJson([...])`                     | 確認 JSON 回應的資料  |
| `assertDatabaseHas('table', [...])`     | 確認資料庫是否存在記錄    |
| `assertDatabaseMissing('table', [...])` | 確認資料庫是否不存在記錄   |

<Info>
  使用 `RefreshDatabase` trait 會在每次測試執行後重設資料庫。用於避免測試間資料互相干擾。
</Info>

## 測試環境

### `phpunit.xml` 的設定

在專案根目錄的 `phpunit.xml` 中做測試環境設定。
預設 session 與 cache 都設為 `array` driver，測試執行中不會留下資料。

```xml theme={null}
<env name="APP_ENV" value="testing"/>
<env name="CACHE_STORE" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
```

資料庫通常使用不留檔的 SQLite in-memory 資料庫：

```xml theme={null}
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
```

### `.env.testing` 檔案

在專案根目錄建立 `.env.testing`，執行測試時會取代 `.env` 讀入。
可用於分開測試專用的資料庫連線或外部服務設定。

```ini theme={null}
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
```

<Warning>
  若設定被快取，請執行 `php artisan config:clear` 後再執行測試。否則舊快取可能造成設定未反映。
</Warning>

## 平行測試

測試數量增加後執行時間會拉長。
用 `--parallel` 選項可用多個 process 平行執行測試以縮短時間。

先安裝 `brianium/paratest` 套件。

```shell theme={null}
composer require brianium/paratest --dev
```

之後加上 `--parallel` 選項執行：

```shell theme={null}
php artisan test --parallel
```

預設會啟動 CPU 核心數的 process。要指定 process 數，用 `--processes` 選項。

```shell theme={null}
php artisan test --parallel --processes=4
```

<Tip>
  使用平行測試時，各 process 需使用獨立的資料庫。搭配 `RefreshDatabase` trait 與 `phpunit.xml` 的 in-memory SQLite 設定即可正常運作。
</Tip>

## 後續步驟

<Card title="HTTP 測試" icon="arrow-right" href="/zh-TW/http-tests">
  可確認請求模擬、附帶認證的測試、JSON 斷言等更詳細的 HTTP 測試方式。
</Card>


## Related topics

- [HTTP 測試](/zh-TW/http-tests.md)
- [使用 Pest 進行進階測試](/zh-TW/advanced/testing-pest.md)
- [Eloquent Factories](/zh-TW/eloquent-factories.md)
- [Laravel Nightwatch 入門](/zh-TW/blog/nightwatch-introduction.md)
- [測試](/zh-TW/packages/laravel-bluesky/testing.md)
