為了構建更好的Laravel應用程式請勿跳過單元測試

為了構建更好的Laravel應用程式請勿跳過單元測試

單元測試在軟體開發中至關重要,它能確保應用程式的各個元件在隔離狀態下按預期執行。通過為特定程式碼單元編寫測試,您可以在開發早期發現並修復錯誤,從而開發出更可靠、更穩定的軟體。

在持續整合/持續交付(CI/CD)管道中,您可以在更改程式碼庫後自動執行這些測試。這可確保新程式碼不會引入錯誤或破壞現有功能。

本文強調了單元測試在 Laravel 應用程式中的重要性,詳細介紹瞭如何為 Laravel 應用程式編寫單元測試。

PHPUnit 簡介

PHPUnit 是 PHP 生態系統中廣泛使用的測試框架,專為單元測試而設計。它擁有一套用於建立和執行測試的強大工具,是確保程式碼庫可靠性和質量的重要資源。

Laravel 支援使用 PHPUnit 進行測試,並提供了方便的輔助方法,讓你可以測試應用程式。

在 Laravel 專案中設定 PHPUnit 只需極少的配置。Laravel 提供了一個預配置的測試環境,包括一個 phpunit.xml 檔案和一個專門的 tests 檔案目錄。

你也可以修改 phpunit.xml 檔案,定義自定義選項,獲得量身定製的測試體驗。您也可以在專案根目錄下建立 .env.testing 環境檔案,而不是使用 .env 檔案。

Laravel 中的預設測試佈局

Laravel 提供了一個結構化的預設目錄佈局。Laravel 專案的根目錄包含一個 tests 目錄,其中有 FeatureUnit 子目錄。這種佈局可以簡單地分離不同的測試型別,並保持一個整潔有序的測試環境。

Laravel 專案中的 phpunit.xml 檔案對於協調測試過程、確保測試執行的一致性以及根據專案要求定製 PHPUnit 的行為至關重要。它允許你定義如何執行測試,包括定義測試套件、指定測試環境和設定資料庫連線。

該檔案還指定會話、快取和電子郵件應設定為陣列驅動程式,以確保在執行測試時不會持續存在會話、快取或電子郵件資料。

注:driver 一詞指的是一種配置設定,在測試過程中將資料儲存在記憶體陣列中,以保持隔離並防止測試執行之間的資料持久化。

你可以在 Laravel 應用程式上執行幾種型別的測試:

  • 單元測試 – 主要針對程式碼的各個元件,如類、方法和函式。這些測試與 Laravel 應用程式隔離,驗證特定程式碼單元是否按預期執行。請注意,在 tests/Unit 目錄中定義的測試不能啟動 Laravel 應用程式,這意味著它們不能訪問資料庫或框架提供的其他服務。
  • 功能測試 – 驗證應用程式更廣泛的功能。這些測試模擬 HTTP 請求和響應,讓你測試路由、控制器和各種元件的整合。功能測試有助於確保應用程式的不同部分按預期協同工作。
  • 瀏覽器測試 – 通過自動化瀏覽器互動,更進一步。測試使用瀏覽器自動化和測試工具 Laravel Dusk 來模擬使用者互動,如填寫表格和點選按鈕。瀏覽器測試對於驗證應用程式在真實瀏覽器中的行為和使用者體驗至關重要。

測試驅動開發概念

測試驅動開發(TDD)是一種軟體開發方法,強調在執行程式碼前進行測試。這種方法遵循一個稱為 “red-green-refactor(紅-綠-重構)” 迴圈的過程。

測試驅動的開發週期顯示為 "紅-綠-重構"

 

測試驅動的開發週期顯示為 “紅-綠-重構”。

以下是對該週期的解釋:

  • 紅色階段 – 在實現實際程式碼之前,編寫一個新測試來定義功能或改進現有功能。測試應該失敗(”紅色” 表示失敗),因為沒有相應的程式碼使其通過。
  • 綠色階段 – 編寫足夠的程式碼使失敗的測試通過,將其從紅色變為綠色。程式碼不會是最佳的,但它能滿足相應測試用例的要求。
  • 重構階段 – 在不改變程式碼行為的前提下,重構程式碼以提高其可讀性、可維護性和效能。在這個階段,你可以放心地修改程式碼,而不必擔心任何迴歸問題,因為現有的測試用例會捕捉到它們。

TDD 有以下幾個好處:

  • 早期錯誤檢測 – TDD 有助於在開發過程的早期發現錯誤,從而減少在開發週期後期修復問題的成本和時間。
  • 改進設計 – TDD 鼓勵使用模組化和鬆散耦合的程式碼來改進軟體設計。它鼓勵你在實施前考慮介面和元件的互動。
  • 重構的信心 – 您可以放心地重構程式碼,因為您知道現有的測試可以快速識別重構過程中引入的任何迴歸。
  • 活文件 – 測試用例提供了程式碼行為的示例,可作為活文件使用。由於測試失敗表明程式碼中存在問題,因此這種文件始終是最新的。

在 Laravel 開發中,你可以應用 TDD 原則,在實現控制器、模型和服務等元件之前為它們編寫測試用例。

Laravel 的測試環境,包括 PHPUnit,提供了方便的方法和斷言,以促進 TDD,確保您可以建立有意義的測試,並有效地遵循紅-綠-重構迴圈。

單元測試的基本示例

本節將介紹如何編寫一個簡單的測試來檢查模型的功能。

前提條件

要繼續學習,你需要具備以下條件:

  • 滿足 Laravel 部落格指南中列出的先決條件。
  • 一個 Laravel 應用程式。本教程使用上面連結的指南中建立的應用程式。你可以閱讀它並建立部落格應用程式,但如果你只需要原始碼來實現測試,請按照下面的步驟進行。
  • 安裝並配置 Xdebug,並啟用覆蓋模式

設定專案

  1. 在終端視窗中執行此命令克隆專案。
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    git clone https://github.com/VirtuaCreative/kinsta-laravel-blog.git
    git clone https://github.com/VirtuaCreative/kinsta-laravel-blog.git
    git clone https://github.com/VirtuaCreative/kinsta-laravel-blog.git
  2. 移動到專案資料夾,執行 composer install 命令安裝專案依賴項。
  3. env.example 檔案重新命名為 .env
  4. 執行 php artisan key:generate 命令生成應用金鑰。

建立並執行測試

首先,確保您的機器上有專案程式碼。您要測試的模型是 app/Http/Models/Post.php 檔案中定義的 Post 模型。該模型包含多個可填寫屬性,如 titledescription, 和 image

您的任務是為該模型設計簡單明瞭的單元測試。其中一個測試驗證屬性是否設定正確,另一個測試則通過嘗試分配不可填充屬性來檢查批量分配。

  1. 執行 php artisan make:test PostModelFunctionalityTest --unit 命令建立新的測試用例。--unit 選項指定這是一個單元測試,並將其儲存在 tests/Unit 目錄中。
  2. 開啟 tests/Unit/PostModelFunctionalityTest.php 檔案,用以下程式碼替換 test_example 函式:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    public function test_attributes_are_set_correctly()
    {
    // create a new post instance with attributes
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // check if you set the attributes correctly
    $this->assertEquals('Sample Post Title', $post->title);
    $this->assertEquals('Sample Post Description', $post->description);
    $this->assertEquals('sample_image.jpg', $post->image);
    }
    public function test_non_fillable_attributes_are_not_set()
    {
    // Attempt to create a post with additional attributes (non-fillable)
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    'author' => 'John Doe',
    ]);
    // check that the non-fillable attribute is not set on the post instance
    $this->assertArrayNotHasKey('author', $post->getAttributes());
    }
    public function test_attributes_are_set_correctly() { // create a new post instance with attributes $post = new Post([ 'title' => 'Sample Post Title', 'description' => 'Sample Post Description', 'image' => 'sample_image.jpg', ]); // check if you set the attributes correctly $this->assertEquals('Sample Post Title', $post->title); $this->assertEquals('Sample Post Description', $post->description); $this->assertEquals('sample_image.jpg', $post->image); } public function test_non_fillable_attributes_are_not_set() { // Attempt to create a post with additional attributes (non-fillable) $post = new Post([ 'title' => 'Sample Post Title', 'description' => 'Sample Post Description', 'image' => 'sample_image.jpg', 'author' => 'John Doe', ]); // check that the non-fillable attribute is not set on the post instance $this->assertArrayNotHasKey('author', $post->getAttributes()); }
    public function test_attributes_are_set_correctly()
    {
    // create a new post instance with attributes
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // check if you set the attributes correctly
    $this->assertEquals('Sample Post Title', $post->title);
    $this->assertEquals('Sample Post Description', $post->description);
    $this->assertEquals('sample_image.jpg', $post->image);
    }
    public function test_non_fillable_attributes_are_not_set()
    {
    // Attempt to create a post with additional attributes (non-fillable)
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    'author' => 'John Doe',
    ]);
    // check that the non-fillable attribute is not set on the post instance
    $this->assertArrayNotHasKey('author', $post->getAttributes());
    }

    這段程式碼定義了兩個測試方法。

    第一個方法建立一個帶有指定屬性的 Post 例項,並使用 assertEquals 斷言方法斷言你正確設定了 titledescription, 和 image 屬性。

    第二個方法嘗試建立一個帶有額外不可填充屬性(author)的 Post 例項,並使用 assertArrayNotHasKey 斷言方法斷言模型例項上未設定該屬性。

  3. 確保在同一檔案中新增以下 use 語句:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use App\Models\Post;
    use App\Models\Post;
    use App\Models\Post;
  4. 執行 php artisan config:clear 命令清除配置快取。
  5. 要執行這些測試,請執行以下命令:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    php artisan test tests/Unit/PostModelFunctionalityTest.php
    php artisan test tests/Unit/PostModelFunctionalityTest.php
    php artisan test tests/Unit/PostModelFunctionalityTest.php

    所有測試都應通過,終端應顯示結果和執行測試的總時間。

除錯測試

如果測試失敗,可以按照以下步驟進行除錯:

  1. 檢視終端中的錯誤資訊。Laravel 提供了詳細的錯誤資訊,可以指出問題所在。仔細閱讀錯誤資訊,瞭解測試失敗的原因。
  2. 檢查正在測試的測試和程式碼,找出差異。
  3. 確保正確設定測試所需的資料和依賴關係。
  4. 使用除錯工具(如 Laravel 的 dd() 函式)檢查測試程式碼中特定點的變數和資料。
  5. 確定問題所在後,進行必要的修改並重新執行測試,直到通過為止。

測試與資料庫

Laravel 提供了一種方便的方法,使用記憶體 SQLite 資料庫來建立測試環境,這種資料庫速度快,而且不會在測試執行之間持久化資料。要配置測試資料庫環境並編寫與資料庫互動的測試,請按以下步驟操作:

  1. 開啟 phpunit.xml 檔案,取消下面幾行程式碼的註釋:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
  2. 執行 php artisan make:test PostCreationTest --unit 命令建立新的測試用例。
  3. 開啟 tests/Unit/PostCreationTest.php 檔案,用下面的程式碼替換 test_example 方法:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    public function testPostCreation()
    {
    // Create a new post and save it to the database
    $post = Post::create([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // Retrieve the post from the database and assert its existence
    $createdPost = Post::find($post->id);
    $this->assertNotNull($createdPost);
    $this->assertEquals('Sample Post Title', $createdPost->title);
    }
    public function testPostCreation() { // Create a new post and save it to the database $post = Post::create([ 'title' => 'Sample Post Title', 'description' => 'Sample Post Description', 'image' => 'sample_image.jpg', ]); // Retrieve the post from the database and assert its existence $createdPost = Post::find($post->id); $this->assertNotNull($createdPost); $this->assertEquals('Sample Post Title', $createdPost->title); }
    public function testPostCreation()
    {
    // Create a new post and save it to the database
    $post = Post::create([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // Retrieve the post from the database and assert its existence
    $createdPost = Post::find($post->id);
    $this->assertNotNull($createdPost);
    $this->assertEquals('Sample Post Title', $createdPost->title);
    }
  4. 確保新增了以下 use  宣告:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use App\Models\Post;
    use App\Models\Post;
    use App\Models\Post;

    目前, PostCreationTest 類擴充套件了 PHPUnitFrameworkTestCase 基類。該基類通常用於在 Laravel 之外直接使用 PHPUnit 進行單元測試,或為與 Laravel 不緊密耦合的元件編寫測試。然而,你需要訪問資料庫,這意味著你必須修改 PostCreationTest 類來擴充套件 TestsTestCase 類。

    後者是為 Laravel 應用程式量身定製的 PHPUnitFrameworkTestCase 類。它提供了額外的功能和 Laravel 特有的設定,如資料庫播種和測試環境配置。

  5. 確保將 use PHPUnitFrameworkTestCase; 語句替換為 use TestsTestCase;。請記住,您設定的測試環境使用的是記憶體 SQLite 資料庫。因此,必須在執行測試前遷移資料庫。請使用 IlluminateFoundationTestingRefreshDatabase 特性來完成這項工作。如果模式不是最新的,該特性就會遷移資料庫,並在每次測試後重置資料庫,以確保上一次測試的資料不會干擾後續測試。
  6. tests/Unit/PostCreationTest.php 檔案中新增以下 use 語句,以便在程式碼中使用該特質:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use Illuminate\Foundation\Testing\RefreshDatabase;
    use Illuminate\Foundation\Testing\RefreshDatabase;
    use Illuminate\Foundation\Testing\RefreshDatabase;
  7. 接下來,在 testPostCreation 方法之前新增以下程式碼行:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use RefreshDatabase;
    use RefreshDatabase;
    use RefreshDatabase;
  8. 執行 php artisan config:clear 命令清除配置快取。
  9. 要執行此測試,請執行以下命令:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    php artisan test tests/Unit/PostCreationTest.php
    php artisan test tests/Unit/PostCreationTest.php
    php artisan test tests/Unit/PostCreationTest.php

    測試應該通過,終端應該顯示測試結果和總測試時間。

功能測試

單元測試是孤立地檢查單個應用程式元件,而功能測試則是檢查程式碼中較大的部分,如多個物件如何互動。功能測試至關重要,原因如下:

  1. 端到端驗證 – 確認整個功能的無縫執行,包括控制器、模型、檢視甚至資料庫等不同元件之間的互動。
  2. 端到端測試 – 涵蓋從初始請求到最終響應的整個使用者流,從而發現單元測試可能遺漏的問題。這種能力使它們在測試使用者旅程和複雜場景時非常有價值。
  3. 使用者體驗保證 – 模擬使用者互動,幫助驗證一致的使用者體驗以及功能是否符合預期。
  4. 迴歸檢測 – 在引入新程式碼時捕捉迴歸和程式碼破壞性更改。如果現有功能在功能測試中開始失效,則表明有東西被破壞了。

現在,在 app/Http/Controllers/PostController.php 檔案中為 PostController 建立一個功能測試。重點是 store 方法,驗證輸入的資料,在資料庫中建立並儲存文章。

該測試模擬使用者通過網路介面建立新文章,確保程式碼將文章儲存在資料庫中,並在建立後將使用者重定向到文章索引頁面。為此,請執行以下步驟:

  1. 執行 php artisan make:test PostControllerTest 命令,在 tests/Features 目錄下建立一個新的測試用例。
  2. 開啟 tests/Feature/PostControllerTest.php 檔案,用以下程式碼替換 test_example 方法:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use RefreshDatabase; // Refresh the database after each test
    public function test_create_post()
    {
    // Simulate a user creating a new post through the web interface
    $response = $this->post(route('posts.store'), [
    'title' => 'New Post Title',
    'description' => 'New Post Description',
    'image' => $this->create_test_image(),
    ]);
    // Assert that the post is successfully stored in the database
    $this->assertCount(1, Post::all());
    // Assert that the user is redirected to the Posts Index page after post creation
    $response->assertRedirect(route('posts.index'));
    }
    // Helper function to create a test image for the post
    private function create_test_image()
    {
    // Create a mock image file using Laravel's UploadedFile class
    $file = UploadedFile::fake()->image('test_image.jpg');
    // Return the path to the temporary image file
    return $file;
    }
    use RefreshDatabase; // Refresh the database after each test public function test_create_post() { // Simulate a user creating a new post through the web interface $response = $this->post(route('posts.store'), [ 'title' => 'New Post Title', 'description' => 'New Post Description', 'image' => $this->create_test_image(), ]); // Assert that the post is successfully stored in the database $this->assertCount(1, Post::all()); // Assert that the user is redirected to the Posts Index page after post creation $response->assertRedirect(route('posts.index')); } // Helper function to create a test image for the post private function create_test_image() { // Create a mock image file using Laravel's UploadedFile class $file = UploadedFile::fake()->image('test_image.jpg'); // Return the path to the temporary image file return $file; }
    use RefreshDatabase; // Refresh the database after each test
    public function test_create_post()
    {
    // Simulate a user creating a new post through the web interface
    $response = $this->post(route('posts.store'), [
    'title' => 'New Post Title',
    'description' => 'New Post Description',
    'image' => $this->create_test_image(),
    ]);
    // Assert that the post is successfully stored in the database
    $this->assertCount(1, Post::all());
    // Assert that the user is redirected to the Posts Index page after post creation
    $response->assertRedirect(route('posts.index'));
    }
    // Helper function to create a test image for the post
    private function create_test_image()
    {
    // Create a mock image file using Laravel's UploadedFile class
    $file = UploadedFile::fake()->image('test_image.jpg');
    // Return the path to the temporary image file
    return $file;
    }

    test_create_post 函式通過向 posts.store 路由發出帶有特定屬性的 POST 請求,模擬使用者建立新文章的過程,其中包括使用 Laravel 的 UploadedFile 類生成的模擬圖片。

    然後,測試通過檢查 Post::all() 的計數,斷言程式碼已成功將文章儲存到資料庫中。它驗證了程式碼是否在建立文章後將使用者重定向到文章索引頁面。

    該測試可確保文章建立功能正常工作,應用程式能正確處理資料庫互動和文章提交後的重定向。

  3. 在同一檔案中新增以下 use 語句:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    use App\Models\Post;
    use Illuminate\Http\UploadedFile;
    use App\Models\Post; use Illuminate\Http\UploadedFile;
    use App\Models\Post;
    use Illuminate\Http\UploadedFile;
  4. 執行命令 php artisan config:clear 清除配置快取。
  5. 要執行此測試,請執行以下命令:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    php artisan test tests/Feature/PostControllerTest.php
    php artisan test tests/Feature/PostControllerTest.php
    php artisan test tests/Feature/PostControllerTest.php

    測試應該通過,終端應該顯示測試結果和執行測試的總時間。

確認測試覆蓋率

測試覆蓋率是指單元測試、功能測試或瀏覽器測試在程式碼庫中的檢查範圍,以百分比表示。它能幫助你識別程式碼庫中未經測試的區域,以及可能包含錯誤的測試不足區域。

PHPUnit 的程式碼覆蓋率功能和 Laravel 的內建覆蓋率報告等工具會生成報告,顯示測試覆蓋了程式碼庫中的哪些部分。這一過程提供了有關測試質量的重要資訊,有助於您關注可能需要額外測試的區域。

生成報告

  1. 刪除 tests/Feature/ExampleTest.phptests/Unit/ExampleTest.php,因為您沒有修改它們,它們可能會導致錯誤。
  2. 在終端視窗中執行 php artisan test --coverage 命令。您應該會收到類似下面的輸出:<img src=”https://kinsta.com/wp-content/uploads/2024/03/code-coverage-report.png” alt=”Screen capture showing the execution of the command php artisan test --coverage. It shows the total number of tests that passed and the time to execute the results. It also lists each component in your codebase and its code coverage percentage.” width=”1001″ height=”471″ />。執行命令 php artisan test --coverage.code 覆蓋率報告會顯示測試結果、通過的測試總數以及執行結果所需的時間。它還會列出程式碼庫中的每個元件及其程式碼覆蓋率。百分比表示測試覆蓋程式碼的比例。例如, Models/Post 的覆蓋率為 100%,這意味著該模型的所有方法和程式碼行都已覆蓋。程式碼覆蓋率報告還會顯示 “Total Coverage“–整個程式碼庫的總體程式碼覆蓋率。在本例中,測試只覆蓋了 65.3% 的程式碼。
  3. 要指定最小覆蓋率閾值,請執行 php artisan test --coverage --min=85 命令。 該命令設定的最小閾值為 85%。您將收到以下輸出:測試最低閾值為 85%測試最低閾值為 85%。

    測試套件失敗的原因是程式碼沒有達到 85% 的最低閾值。雖然實現更高的程式碼覆蓋率(通常是 100%)是我們的目標,但更重要的是徹底測試應用程式的關鍵和複雜部分。

小結

通過採用本文概述的最佳實踐,如編寫有意義的綜合測試、堅持 TDD 中的紅-綠-重構迴圈,以及利用 Laravel 和 PHPUnit 提供的測試功能,你可以建立健壯而高質量的應用程式。

評論留言