10分鐘掌握Laravel的測試機制

10分鐘掌握Laravel的測試機制

簡介

我們知道,Laravel 框架所提供的測試功能是基於 PHPUnit 來實作的,PHPUnit 是 PHP 語言中最負盛名的單元測試框架

在介紹 Laravel 框架提供的測試功能之前,讓我們先從源頭 PHPUnit 開始聊聊如何在 PHP 框架中實現單元測試

即使是很小規模的項目開發也需要數小時的辛苦編碼。在開發過程中,產品程式中或多或少都會存在一些大大小小的問題,開發者往往會嘗試在開發過程中解決這些問題,以便順利上線。但問題是,在未經完整測試的情況下,沒有一種方式可以確保最終上線程式碼中沒有任何問題。此外,也沒有辦法確保修復老問題之餘會不會引發新問題

為了一次性解決上述問題,我們需要在開發過程中引入測試流程,並將其作為日常開發流程中不可或缺的一部分,以確保程式碼品質

在現代程式開發流程中,測試驅動開發、持續優化這些概念都將測試作為開發流程的基本組成部分,要求我們在程式開發的一開始,就要設計好相關的測試方法,從而讓程式碼更加易於擴展、反覆和維護,實際上 Laravel 框架也是這麽設計的,它內建了一系列測試相關的 API 以方便我們對程式碼進行測試

Laravel 內建就支援單元測試,並已經為你的應用配置好了 phpunit.xml 檔案

//phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap="vendor/autoload.php" colors = "true" verbose="true" stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>./tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

其中 colors="true" 表示測試結果會以高亮顯示,<directory>./tests/Unit</directory> 則用於指定測試用例所存放的資料夾

注: 更多設定訊息请参考 PHPUnit 官方文件(目前只找到簡體中文版)

框架還提供了一些便利的幫助函數來讓你更直觀的測試你的應用

預設情況下,應用的 tests 目錄中包含兩個子目錄:Feature 和 Unit

單元測試(Unit Tests)是針對你的程式碼中非常少,而且相對獨立的部分來進行的測試。實際上,大部分單元測試都是針對單個方法進行的。單元測試不會啟動你的應用因此無法取用應用資料庫或框架的服務

功能測試(Feature Tests)則是針對大部分的程式碼進行的測試,包括多個物件之間的交互,甚至是對 JSON 端點的完整 HTTP 請求等等。總的來說,你大部分的測試都應該是功能測試,這些測試能讓你確保你的系統能夠正常運作

Feature 和 Unit 目錄中都提供一個 ExampleTest.php 測試範例文件。安裝一個新的 Laravel 應用之後,在命令行下運行 phpunit 或者是 test 命令,即可運行測試

php artisan test

環境

在使用 phpunit 進行測試時,Laravel 將根據 phpunit.xml 文件設定的環境變數自動將環境設置為 testing,並將 Session 及緩存以 array 的驅動形式來保存,也就是說在測試時不會持久保存任何 Session 或緩存資料

你可以隨意創建其它必要的測試環境配置。testing 環境變數可以在 phpunit.xml 文件中修改,但是在運行測試之前,請確保使用以下命令來清除配置的緩存!

php artisan config:clear

.env.testing 檔案

此外,你還可以在你的專案根目錄下創建一個 .env.testing 檔案,在運行單元測試或使用帶有 --env=testing 選項的 Artisan 命令時, .env 文件中的變數將會被這個文件覆蓋

快速開始

生成一個測試用例:

在編寫測試用例之前,我們需要了解關於編寫單元測試的一些常見規格:

  • 測試文件名稱需要以 Test 作為後綴,比如要測試 First.php,則對應的測試文件名為 FirstTest.php
  • 測試方法名稱需要以 test 作為前綴,比如要測試的方法名為 getUser,則對應的測試方法名為 testGetUser,此外,你也可以通過 @test 註解來聲明此為測試方法
  • 所有測試方法的可見性必須是 public
  • 所有單元測試類別都繼承自 PHPUnit\Framework\TestCase
  • 所有功能測試類別都繼承自 Tests\TestCase

使用 Artisan 命令 make:test 創建一個新的測試用例:

// 在 Feature 目錄下創建一個測試類別...
php artisan make:test UserTest

// 在 Unit 目錄下創建一個測試類別...
php artisan make:test UserTest --unit

技巧:

可以使用 stub publishing 自定義測試 stub

測試類別一旦生成,你就可以像使用 PHPUnit 一樣定義測試方法。 執行 phpunit 或者 php artisan test 命令即可執行測試:

//tests\Unit\ExampleTest.php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase; 

class ExampleTest extends TestCase
{
    /**
     * 一個基礎的測試用例
     *
     * @return void
     */
    public function testBasicTest()
    {
        $this->assertTrue(true);
    }
}

所有的測試方法,都應該以test作為前綴,後面才是測試用例的名稱

注意:

如果要在你的測試類別中定義自己的 setUp / tearDown 方法,請確保呼叫了父類別中的 parent::setUp() / parent::tearDown() 方法。

測試前設定

如果需要為測試進行一些設定,比如測試前重置資料庫資料又或者是跳過中介層等等,都是透過 Trait 的機制來實現。比如下面例子的 RefreshDatabase 能夠幫我們在每次測試前重整資料庫

//tests\Feature\PostTest.php

namespace Tests\Feature;

use Tests\TestCase; //要改用這個
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostTest extends TestCase
{
    use RefreshDatabase; //每次執行時都要重整資料庫

}

執行測試

你可以使用 phpunit 執行檔來執行測試如下:

./vendor/bin/phpunit

除 phpunit 命令之外, 您還可以使用 Artisan 的 test 命令來運行你的測試。Artisan 測試運行器提供了關於當前正在運行的測試的更多訊息,以便於日常開發與調試,推薦使用:

php artisan test

此外,任何可以傳遞給 phpunit 命令的參數也可以傳遞給 Artisan 的 test 命令:

php artisan test --testsuite=feature --stop-on-failure

實作展示

Step 1.生成 posts 表格

建立 posts 表格的 Migration 檔案

php artisan make:migration create_posts_table

編輯 Migration 內容如下:

//database\migrations\xxxx_xx_xx_xxxxxx_create_posts_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title', 50);
            $table->longText('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

執行 migrate 來生成 posts 表格

php artisan migrate

生成 Post.php 作為 Eloquent 模型

php artisan make:model Post

Step 2.建立假資料工廠

建立 PostFactory.php 檔案,該檔案用來定義生成的欄位資料

php artisan make:factory PostFactory

編輯 PostFactory.php 的內容如下:

//database\factories\PostFactory.php

namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title' => $this->faker->word,
            'content' => $this->faker->paragraph
        ];
    }
}

Step 3.新增&編輯控制器

新增 PostController.php 用來處理 posts 表格的 CRUD

輸入以下指令來新增空白的 PostController.php

php artisan make:controller PostController

編輯 PostController.php 內容如下:

//app\Http\Controllers\PostController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;
use Symfony\Component\HttpFoundation\Response;

class PostController extends Controller
{
    public function index(){
        $posts = Post::get();
        return json_encode($posts, JSON_UNESCAPED_UNICODE);
    }

    public function show(Request $request,$id){
        $post = Post::findOrFail($id);
        return json_encode($post, JSON_UNESCAPED_UNICODE);
    }

    public function store(Request $request){
        $data = ['title' => $request->title , 'content' => $request->content];

        return response()->noContent(Response::HTTP_CREATED); //回傳201狀態碼
    }

}

Step 4.撰寫路由

在 routes/web.php 加入對應路由

//routes\web.php

Route::get('/posts','App\Http\Controllers\PostController@index');
Route::post('/posts','App\Http\Controllers\PostController@store');
Route::get('/posts/{id}','App\Http\Controllers\PostController@show');

Step 5.新增測試用例

新增一個功能測試用例

php artisan make:test PostTest

PS:如果是想新增單元測試,記得要加上 unit 選項,預設是新增功能測試

你將會發現在 tests/Feature 資料夾將會出現一個新的檔案,名為 PostTest.php

Step 6.編輯測試用例

首先載入 Post 模型 . RefreshDatabase 以及 WithoutMiddleware ,RefreshDatabase 用於重置資料庫,WithoutMiddleware則用於關閉中介層

每一個以 assert 開頭的方法都是用於進行測試,比如 assertTrue() 是用來檢查參數的值是否為 true

//tests\Feature\PostTest.php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class PostTest extends TestCase
{
    use WithoutMiddleware,RefreshDatabase;

    //啟動每次設定前先執行
    public function setUp(): void
    {
        parent::setUp();
        Category::factory(5)->create();
        Schema::disableForeignKeyConstraints(); //關閉外鍵偵測
    }

    //測試是否正確的建立5筆資料
    public function test_posts_count()
    {
        Post::factory()->count(5)->create(); //生成五筆假資料

        $posts = Post::get();

        $this->assertCount(5,$posts); //確認資料筆數
    }

    //測試 /posts 路徑能否正常訪問
    public function test_index_get()
    {
        Post::factory()->count(5)->create(); //生成五筆假資料
        $response = $this->get('/posts');

        $response->assertStatus(200); //確認狀態碼
    }

    //測試 /posts 路徑能否看到指定的標題
    public function test_index_see()
    {
        Post::factory()->count(2)->create(); //生成2筆假資料
        $response = $this->get('/posts');

        $post = Post::first();
        $response->assertSee($post->title);
    }

    //測試 /posts/{id} 路徑能否正常用get訪問
    public function test_show_get()
    {
        Post::factory()->count(5)->create(); //生成五筆假資料
        $post = Post::first();

        $response = $this->get("/posts/{$post->id}");
        $response->assertStatus(200);
    }

    //測試 /posts/store 路徑能否正常用來新增資料
    public function test_store_post()
    {
        $post = Post::factory()->make();
        $response = $this->post('/posts',['title'=>$post['title'],'content'=>$post['content'],'sort'=>$post['sort'],'content_small'=>$post['content_small'],'pic'=>$post['pic'],'status'=>$post['status'] ]);
        $response->assertStatus(201);
    }
}

Step 7.建立測試用設定檔

複製 .env 檔用以生成 .env.testing 檔案,內容大致可相同,一般來說根據需求將資料庫設定改成另一個資料庫

Step 8.執行測試

執行所有測試用例的命令

php artisan test

如果測試成功會顯示綠色的打勾,反之失敗則會顯示紅色,並且說明錯誤的程式碼

進階技巧

並行測試

預設情況下,Laravel 和 PHPUnit 會在單一線程依序地執行每個測試。假如你覺得測試所需要花費的時間過久就可以考慮以多線程並行的方式來大幅縮減測試所需的時間。要進行並行測試,只需要在 test 命令後面加上 --parallel 選項即可

php artisan test --parallel

啟動並行時,預設 Laravel 將會盡可能的使用機器上所有的 CPU,但是你還是可以去限制所要給它使用的數量,透過 --processes 選項

php artisan test --parallel --processes=4

參考資料

HTTP網頁測試 HTTP Tests Assertions

資料庫測試 Database Tests Assertions

瀏覽器操作自動測試 Browse Testing 完整攻略


分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

一小時免費求職講座

3個應徵軟體工程師前該知道的秘訣

取得免費課程連結

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪