簡單的11步在Laravel中實現測試驅動開發

kevinyan發表於2019-02-27

測試驅動開發(英語:Test-driven development,縮寫為TDD)是一種軟體開發過程中的應用方法,由極限程式設計中倡導,以其倡導先寫測試程式,然後編碼實現其功能得名。

下文是我在Medium上看到的一篇文章講述瞭如何在Laravel中實現測試驅動開發,我自己已經在專案中實現了測試驅動開發,並且通過持續整合完成了專案的自動測試,文章中有些我認為需要改進的地方也註明了,希望這篇文章能幫助到那些想寫測試用例但是一直覺得無從下手的開發者們。文章一共有四篇,之後計劃都翻譯出來給大家參考。

原文連結: https://medium.com/@jsdecena/simple-tdd-in-laravel-with-11-steps-c475f8b1b214

前言

大多數Web開發工程師第一次聽到TTD(test driven development)測試驅動開發的時候都會感到畏懼,我也一樣。當你剛開始試著進行測試驅動開發時你會感到很崩潰,但是如果你抗拒它就很難真正學會並掌握它。在本文中我會介紹如何在Laravel中進行測試驅動開發。

Note: 這篇文章時針對API響應的TDD. 如果你想看與Laravel blade相關的功能測試請看這篇文章 (還未來得及翻譯,訪問需要科學上網), head up on this article.

step 1: 準備Laravel測試套件

在專案根目錄中,更新phpunit.xml檔案的如下專案:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="API_DEBUG" value="false"/>
<ini name="memory_limit" value="512M" />
複製程式碼

更新完後phpunit.xml檔案會像下面這樣:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="APP_DEBUG" value="false"/>
        <env name="MAIL_DRIVER" value="log"/>
        <ini name="memory_limit" value="512M" />
    </php>
</phpunit>
複製程式碼

我們只需要在記憶體中進行測試這樣測試執行的速度會快一些,所以在database配置專案中我們將使用sqlite:memory: (Sqlite的記憶體資料庫)。 將APP_DEBUG設定為false因為我們只需要對真實產生的錯誤進行斷言。隨著專案迭代測試用例會越來越多所以在將來你可能會需要增加memory_limit的值。

譯者注: APP_DEBUG建議不要改成false,這樣有助於讓我們的程式碼寫的更嚴謹

在Laravel裡測試用例的基類TestCase中作一些測試相關的準備:

<?php
namespace Tests;
use IlluminateFoundationTestingDatabaseMigrations;
use IlluminateFoundationTestingDatabaseTransactions;
use IlluminateFoundationTestingTestCase as BaseTestCase;
use FakerFactory as Faker;
/**
 * Class TestCase
 * @package Tests
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseMigrations, DatabaseTransactions;
    protected $faker;
    /**
     * Set up the test
     */
    public function setUp()
    {
        parent::setUp();
        $this->faker = Faker::create();
    }
    /**
     * Reset the migrations
     */
    public function tearDown()
    {
        $this->artisan(`migrate:reset`);
        parent::tearDown();
    }
}
複製程式碼

我們需要在TestCase中use DatabaseMigrations這個trait這樣在執行每個測試用例時,遷移檔案都會被執行一遍,於此同時在setUp()tearDown()方法中我們需要執行建立測試環境和清理測試環境的相關操作。

譯者注: DatabaseMigrations這個性狀在測試setUp階段會執行migrate:refresh, 清除所有表然後重新執行遷移, 其實我們重置資料庫後需要通過seeder來填充測試資料,所以我更傾向於不使用這個遷移而是在TestCase的setUp中 使用migrate:refresh —seeder 來完成重置資料庫和填充測試資料的操作

Step2:寫測試用例

就像鮑勃大叔說的那樣:”除非先寫測試用例否則你沒有權利去寫實現程式碼(implementation)“。現在開始寫我們的測試吧。(測試驅動開發的第一天原則)

為了讓phpunit識別測試,需要在測試方法上新增/** @test **/註釋,或者是測試方法命名以test字首開頭。

<?php
namespace TestsUnit;
use TestsTestCase;
class ArticleApiUnitTest extends TestCase
{
  public function it_can_create_an_article()
  {
      $data = [
        `title` => $this->faker->sentence,
        `content` => $this->faker->paragraph
      ];
    
      $this->post(route(`articles.store`), $data)
        ->assertStatus(201)
        ->assertJson($data);
  }
}
複製程式碼

在這個測試中,測試了是否能建立一篇文章,我們斷言了在建立文章成功後應用將返回201狀態碼還有預期的JSON資料。

在建立好我們的第一個測試後,執行phpunit或者vendor/bin/phpunit

圖片1

當我們執行phpunit後測試結果顯示失敗了,這很正常因為在測試驅動開發中我們是先寫測試程式,然後在編碼實現功能的,所以在建立測試程式伊始測試程式執行後的結果就是測試失敗(測試驅動開發的第二條原則)。在測試中我們斷言應用會返回201狀態碼但是卻返回了404,為什麼? 因為測試中請求的URL還未在應用中建立。這個api/v1/articles POST路由在應用中並不存在所以針對這個請求應用丟擲了404錯誤。

接下來我們該作什麼?

Step 3: 在路由檔案中建立測試裡請求的URL

讓我們建立測試裡請求的這個URL看看接下來會發生什麼。

在你專案中的routes/api.php檔案中建立這個URL,在api中的路由其URL會自動加上/api字首。

<?php
use AppHttpControllersApiArticlesApiController;
use IlluminateSupportFacadesRoute;
Route::group([`prefix` => `v1`], function () {
  Route::resource(`articles`, ArticlesApiController::class);
});
複製程式碼

你可以通過artisan命令建立這個資源控制器:

php artisan make:controller ArticlesApiController —-resource
複製程式碼

也可以手動建立。POST請求將會路由到ArticlesApiContorllerstore方法

Step 4: DEBUG 控制器

<?php
namespace AppHttpControllersApi;
class ArticlesApiController extends Controller 
{
    public function store() {
        dd(`success!`);
    }
}
複製程式碼

現在讓我們執行測試程式,看看測試程式是否能夠訪問到應用的這一部分,再次執行phpunit後返回結果如下:

圖片2

執行後的結果在終端中提示出了字串success!這意味著測試程式成功地訪問到了建立文章的API。

Step 5: 驗證你的輸入

不要忘記驗證將要儲存到資料庫中的資料,所以現在我們建立一個CreateArticleRequest類來控制輸入資料的驗證。

<?php
namespace AppHttpControllersApi;
class ArticlesApiController extends Controller 
{
    public function store(CreateArticleRequest $request) {
        dd(`success!`);
    }
}
複製程式碼

這個請求類包含了資料驗證的規則:

<?php
namespace AppArticlesRequests;
use IlluminateFoundationHttpFormRequest;
class CreateArticleRequest extends FormRequest
{
    /**
     * Transform the error messages into JSON
     *
     * @param array $errors
     * @return IlluminateHttpJsonResponse
     */
    public function response(array $errors)
    {
        return response()->json($errors, 422);
    }
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            `title` => [`required`],
            `content` => [`required`]
        ];
    }
}
複製程式碼

這是一個很好的驗證資料的方式,因為之後我們可以新建另外一個測試程式來測試是否能捕獲到這些驗證錯誤,不過為了文章的精簡我會在另外一篇單獨的文章裡來講述如何建立捕獲錯誤的測試程式。

Step 6: 返回剛才建立的資料

記住在相應中應該返回指定的JSON結構這樣我們就知道新建的資料成功儲存到了資料庫中。所以我們應該返回新建立的文章物件來滿足我們上面建立的測試程式。

<?php
namespace AppHttpControllersApi;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all());
    }
}
複製程式碼

你可能已經已經注意到了我們還沒有建立Article這個類,接下來讓我們來建立這個Model。

Step 7: 建立模型類

<?php
namespace AppArticles;
use IlluminateDatabaseEloquentModel;
class Article extends Model 
{
  protected $fillable = [
    `title`,
    `content`
  ];
  
}
複製程式碼

在Article這個模型類中,你需要定義可填充和序列化時需要被隱藏的欄位,一旦Article類定義好後,返回到控制器中匯入這個Article類。

<?php
namespace AppHttpControllersApi;
use AppArticlesArticle;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all());
    }
}
複製程式碼

我們差不多快要完成了!測試程式正確建立好了,URL已經建立並且能夠訪問了,處理URL請求的控制器程式還有與資料表對應的模型類也都已經就緒了,現在讓我們再試著執行一次phpunit命令。

Step 8: 再次執行phpunit看看結果會怎麼樣

圖片3

它再一次失敗了,這是好事還是壞事?可以說即是好事也是壞事。好的一方面是測試中斷言會返回201狀態碼的URL的返回結果從之前的404錯誤變成了500錯誤(如果你注意到了)。

不好的一方面是,它測試失敗了我們需要讓程式能夠正確通過測試程式。當我們想要debug時我們想要看看應用程式到底丟擲了什麼錯誤。在Laravel測試程式中你只需要在發出POST請求後在呼叫dump()方法就能夠看到應用程式返回的響應。


<?php
namespace TestsUnit;
use TestsTestCase;
class ArticleApiUnitTest extends TestCase
{
  public function it_can_create_an_article()
  {
      $data = [
        `title` => $this->faker->sentence,
        `content` => $this->faker->paragraph
      ];
    
      $this->post(route(`articles.store`), $data)
        ->dump()
        ->assertStatus(201)
        ->assertJson($data);
  }
}
複製程式碼

你可以進一步debug POST請求後的輸出,沒準會得到更多你需要的資訊,如果沒有明確地給出提示發生了什麼才導致的這個錯誤你可以去Laravel應用的日誌檔案**/storage/logs/laravel.log**裡去查詢錯誤資訊。

現在讓我們檢查一下為什麼會返回500錯誤。

Well, 因為我們在請求中正在嘗試向一個不存在的資料表中寫入資料所以才請求才會返回500錯誤。

Step 9: 建立資料表

執行下面的laravel artisan命令:

php artisan make:migration create_articles_table –create=articles
複製程式碼

Laravel會自動在/database/migrations裡來建立遷移檔案。

<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create(`articles`, function (Blueprint $table) {
            $table->increments(`id`);
            $table->string(`title`);
            $table->text(`content`);
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists(`articles`);
    }
}
複製程式碼

預設遷移建立的資料表中只有id和時間欄位,你需要根據需求新增需要的欄位。

Step 10: 再一次執行phpunit

我們快要大功告成了,文章開始時我們承諾了完成TDD有11個步驟,現在是步驟10了!你需要輕輕拍下你的被來鼓勵自己已經走到了這裡。

圖片4

Ooops, 又失敗了?Whyyyyyyy? 如果你仔細檢查的話你會發現你處在正確的軌道上!響應的狀態碼從500又變成200。這意味著在發出POST請求後應用返回了執行成功的響應!這是沒有匹配我們的需求,我們需要應用返回的響應狀態碼為201來讓我們知道文章資料已經被正確寫入到了資料庫中。所以我們只需要修改一下我們的控制器程式為:

<?php
namespace AppHttpControllersApi;
use AppArticlesArticle;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all(), 201);
    }
}

複製程式碼

Step 11: 執行phpunit然後祈禱一切會好

圖片5

恭喜大功告成!你成功讓程式通過了測試,這就是測試驅動開發的第三條原則。

這就是測試驅動開發(TDD)在Laravel中的簡單實現。還有其它的一些方法在這篇文章中沒有來得及講到,我在未來可能會那些,像是repository pattern等等。repository pattern是DDD (domain driven development)領域驅動開發的最佳實踐。

測試驅動開發還有很多方法這裡沒有設計到不過我覺得這篇文章已經足夠讓你開始試著在實踐中應用測試驅動開發了。

譯者注

通過文章總結起來測試驅動開發有三條原則:

  1. 倡導先寫測試程式,再編碼實現功能。
  2. 測試程式建立伊始肯定會測試失敗。
  3. 在讓測試程式測試成功的過程中逐步編碼實現功能

相關文章