[新手開發記錄] 從測試開始開發

CS33發表於2020-05-23

我準備開發一個名校公開課的資料下載和討論版,功能就是站長髮布資訊和資料下載連結,使用者可以在下面討論,並且可以生成各種平臺的分享方式分享出去。這一系列文章就記錄這一過程。當然博文功能也是必不可少的。

測試驅動開發

測試是一個很重要的內容,我也不是很熟悉,只是跟著教程一步步去做,但是開發的東西不一樣,我想開發我的東西,教程是教一個簡單的網站和複雜的論壇,分別是 Laracasts 的 《Build A Laravel App With TDD》和 《Let’s Build A Forum with Laravel and TDD》,這兩個系列都是在 Laravel From Scratch 之後可以進一步學習以深化技術的質量和深度都不錯的課程,我就擇取重點參考這幾個課程來協助我完成這個網站的構建。
(中文語音版看這裡:[完結] Laravel 6 From Scratch [Laracasts 免費視訊中文語音]

注:TDD 是測試驅動開發(Test-Driven Development)。測試也有很多細分的東西,可以參考這裡:laravel-china.github.io/php-the-rig...

新建測試類

我們在建立好網站的大體樣式以後,馬上就新建一個測試:

php artisan make:test CoursesTest

你可以在 test/Feature/ 目錄下找到。
然後我們就寫我們第一個測試,一個使用者可以新建 course,邏輯是使用者向伺服器的一個 url 提交一個 post 請求,然後就可以在資料通過驗證之後在資料庫建立一個 course。

這裡的 course 是指某一個系列課程的名稱,它還可以屬於某個分類,比如名校公開課,國外視訊教程等等。然後每個 course 又由很多單個的課構成,一般稱之為 episode 。除了構建關係部分,curd 部分實際上是差不多的,我們這裡就以 course 舉例。

整個檔案程式碼此時應該看起來如下:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class CoursesTest extends TestCase
{
    use WithFaker, RefreshDatabase;

    public function a_user_can_create_a_course()
    {
        $attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', $attributes);
    }
}

我們可以看到這類測試類擴充套件了 TestCase 類:

class CoursesTest extends TestCase

那麼它繼承了許多有用的方法,不瞭解的可以仔細看本站的這篇文章參考:Laravel 測試之 —— PHPUnit 入門教程

解釋

我們一段段來看。

第一段:

use WithFaker, RefreshDatabase;

這裡使用了兩個 trait,WithFaker 令我們可以在測試程式碼中使用 $this->faker 來方便的生成各種虛擬資料,十分方便,例如上述程式碼中的 $this->faker->sentence$this->faker->paragraphRefreshDatabase 可以讓我們在執行完測試之後,資料庫恢復測試前的狀態。

第二段:

$this->post('/courses', $attributes);

這裡通過測試例項提供的 post() 方法,模擬嚮應用傳送了一個 POST 請求,把 $attributes 傳入,然後伺服器就對這些資料進行處理。

第三段:

$this->assertDatabaseHas('courses',  $attributes);

這是最終測試的檢測過程,檢測資料庫表 courses 中是否能夠獲取這組資料 $attributes

執行測試

寫完測試,可以通過終端執行:

vendor/bin/phpunit tests/Feature/CoursesTest.php

關於如何簡化命令,要麼設定一個快捷命令(參考本站帖子:讓你懶到逆天的 Bash 別名),或者很多流行的編輯器都帶有外掛功能,可以利用外掛快捷鍵快速執行測試。

那麼這時候肯定會報錯的,因為這時候除了測試什麼都沒有,這也是 laracasts 作者推薦的實際開發流程,也是測試驅動開發的含義,先寫測試,然後根據錯誤一步步的完善程式功能。

存在的幾個問題

這裡顯然存在幾個問題:
1.沒有資料庫
我們需要新建資料庫,並且配置資料庫,配置如下:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

然後我們就需要根據配置檔案中的 DB_DATABASE 的值來新建一個資料庫,我這裡演示一下 laragon 的方法,它自帶一個資料庫管理工具:

[新手開發記錄] 從測試開始開發

點選 Database 按鈕之後,會出來如下介面:

[新手開發記錄] 從測試開始開發

可以看到右側的使用者名稱和密碼還有埠號,和我們的配置一致,這是預設設定。
然後我們雙擊左側 Laragon ,出現如下介面:

[新手開發記錄] 從測試開始開發
我們在 Laragon 處郵件單擊 Laragon 新建資料庫:

[新手開發記錄] 從測試開始開發
填入資料庫名稱,這裡我填了 laravel,選擇了字元編碼,點選確定(OK)就完成了建立了。

[新手開發記錄] 從測試開始開發

2.資料庫沒有 courses 表並且測試執行在我們應用程式的同一個資料庫中
我們就需要做兩件事:
第一,配置測試用的資料庫
我們開啟 ./phpunit.xml 進行一些配置,我擷取一些配置程式碼,下面是原始的配置:

    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>

<php></php> 之間,我們新增一些配置行,name.env 檔案中的配置項相同,這裡配置了之後會覆蓋 .env 中的配置,下面就是我們要新增的配置行:

        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>

第一行是覆蓋了要連線的資料庫型別,測試時,我們使用 sqlite,然後第二行表示資料庫使用記憶體。
這時候我們再次執行測試,仍然會報一樣的錯誤,但是這時候就不再和我們的應用共用資料庫了。
第二,生成我們應用的資料庫表
上面說了,測試還是會報錯,因為我們沒有建立資料庫表,我們需要生成資料庫表格,我們執行下列命令來建立資料庫遷移檔案:

php artisan make:migration create_courses_table

這裡提示一下,上面說的 RefreshDatabase 這個 trait ,它不僅會在測試結束後恢復資料庫狀態,還會在測試前進行必要的資料庫遷移操作。
所以,如果我們再次執行測試,錯誤會再次更新,因為資料庫表有了,我們雖然沒有遷移它,但是測試時候 RefreshDatabase trait 會幫我們遷移,這時候它會尋找上面 $attributes 對應的資料庫欄位,但是沒找到。

但是有個問題,大家有沒有注意到,我們測試一直是執行到最後檢測資料的時候報錯,但是中間的提交資料的程式碼卻從來沒有報錯,就是下面這一段程式碼:

$this->post('/courses', $attributes);

而實際上我們根本沒有建立任何的路由,因此這裡實際上也是存在錯誤或者異常的。
為什麼會這樣呢,因為 laravel 會預設幫我們處理異常,但是測試過程中這會讓我們無法進行有效的檢測,所以我們必須告訴 laravel,不必在意任何錯誤或者異常,都丟擲來。
所以我們要在上述的測試類中再加一行程式碼,變成如下:

public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);
    }

新增的程式碼行是:

        $this->withoutExceptionHandling();

就是阻止了 laravel 預設開啟的異常處理。
這時候我們再測試,就會發現新的錯誤,在沒有執行到最後判斷資料的程式碼行時,已經丟擲異常,沒找到對應的請求路徑。
所以我們要在 ./routes/web.php 中新增一個新的路由,如下:

Route::post('/courses', function(){
    //待寫
});

然後再執行測試,錯誤又回到了剛才的,找不到 $attributes 對應的資料庫欄位。
所以在上面的路由閉包中,我們就需要把 $attributes 資料儲存到資料庫表中。
這就是很傳統的三部曲:

Route::post('/courses', function(){
    //1.驗證資料
    //2.儲存資料
    //3.跳轉網頁
});

驗證的事情我們以後會講,這裡直接講儲存資料,我直接把我希望的方式寫出來:

Route::post('/courses', function(){
    //1.驗證資料
    //2.儲存資料
    App\Course::create(request(['title', 'description']));
    //3.跳轉網頁
});

當然又會報錯,因為我們沒有 App\Course 這個類。
於是執行如下:

php artisan make:model Course

然後繼續測試,下一個錯誤是新手經常碰到的 mess assignment 異常,我們通過批量賦值的操作進行資料庫的新建或者更新,laravel 預設會阻止這一行為。

這個保護機制通常是有用的,但是 laracasts 的作者多次表示它比較煩人,所以經常主動關閉它,不過大家模仿這個操作的時候務必弄清楚這個異常的本質,然後清楚自己在幹什麼。

關閉方法就是在 Course 的類檔案中新增如下程式碼:

class Course extends Model
{
    protected $guarded = [];
}

然後再次測試,錯誤更新了,就是插入資料庫時找不到 title 和 description 欄位。
因為我們還沒有修改我們的資料庫遷移檔案,生成資料庫的時候沒有生成我們需要的 title 和 description 欄位,我們加上,使得資料庫遷移檔案中的 up() 函式如下:

    public function up()
    {
        Schema::create('courses', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description');
            $table->timestamps();
        });
    }

最後再執行一次測試,我們測試成功了。
那麼這個測試的過程,我們再看一下:

    public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);
    }

首先是通過一個 POST 請求,提交了一組資料,然後我們期望在資料庫的 courses 表中看到這組資料。
但是這還不夠,我們應當最後希望能夠在檢視中看到這些資料,比如在一個課程列表中。
所以我們繼續修改我們的測試檔案,在最後新增一行,我們希望在訪問課程列表時,看到我們新增的課程的 title:

    public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);

        $this->get('/courses')->assertSee($attributes['title]);
    }

當然,執行的時候肯定也會報錯,因為程式找不到對應的 url,我們還沒有定義相應的路由。

注:對於已經寫的兩個路由不熟悉的話,可以參考國外教程中文視訊:
1.七個 RESTful 控制器方法
2.RESTful路由
目前的情況,我們還沒有專門的 Course 控制器,都是通過路由閉包實現處理,後面我們會重構。

我們修改路由檔案:

Route::post('/courses', function(){
    $courses = App\Course::all();

    return view('courses.index', compact('courses));
});

執行測試,檢視不存在。
我們就新建一個檢視 /resources/views/courses/index.blade.php

<!DOCTYPE html>
<html>
<head>
    <title>Courses</title>
</head>
<body>

    <h1>Courses</h1>

    <ul>
        @foreach($courses as $course)
        <li>{{ $course->title }}</li>
        @endforeach
    </ul>

</body>
</html>

執行測試,通過!
本文就講到這裡,初步感受一下 TDD 的流程。

最後

測試驅動開發就是一個個的利用測試類提供的功能,測試先行,可用的方法很多,我就列舉了一些,全部細節也不一一列出了。
有興趣的可以關注公眾號 laravelgo,然後根據提示加群,後續如果大家有興趣,我可以把上面說的兩個 TDD 開發系列精華進行整理漢化,漢化內容質量參考我B站更新的內容,精華整理分享應該比同聲傳譯質量更高:B站地址,點選訪問

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章