我準備開發一個名校公開課的資料下載和討論版,功能就是站長髮布資訊和資料下載連結,使用者可以在下面討論,並且可以生成各種平臺的分享方式分享出去。這一系列文章就記錄這一過程。當然博文功能也是必不可少的。
測試驅動開發
測試是一個很重要的內容,我也不是很熟悉,只是跟著教程一步步去做,但是開發的東西不一樣,我想開發我的東西,教程是教一個簡單的網站和複雜的論壇,分別是 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->paragraph
。RefreshDatabase
可以讓我們在執行完測試之後,資料庫恢復測試前的狀態。
第二段:
$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 協議》,轉載必須註明作者和本文連結