使用 TDD 測試驅動開發來構建 Laravel REST API

file

TDD 以及敏捷開發的先驅者之一的 James Grenning有句名言:php

若是你沒有進行測試驅動開發,那麼你應該正在作開發後堵漏的事 - James Grenninghtml

今天咱們將進行一場基於 Laravel 的測試驅動開發之旅。 咱們將建立一個完整的 Laravel REST API,其中包含身份驗證和 CRUD 功能,而無需打開 Postman 或瀏覽器。?laravel

注意:本旅程假定你已經理解了 Laravel 和 PHPUnit 的基本概念。你是否已經明晰了這個問題?那就開始吧。git

項目設置

首先建立一個新的 Laravel 項目 composer create-project --prefer-dist laravel/laravel tdd-journeygithub

而後,咱們須要建立 用戶認證 腳手架,執行  php artisan make:auth ,設置好 .env 文件中的數據庫配置後,執行數據庫遷移 php artisan migrate數據庫

本測試項目並不會使用到剛生成的用戶認證相關的路由和視圖。咱們將使用 jwt-auth。因此須要繼續 安裝 jwt 到項目。json

注意:若是您在執行 jwt:generate 指令時遇到錯誤, 您能夠參考 這裏解決這個問題,直到 jwt 被正確安裝到項目中。api

最後,您須要在 tests/Unittests/Feature 目錄中刪除 ExampleTest.php 文件,使咱們的測試結果不被影響。數組

編碼

  1. 首先將 JWT 驅動配置爲 auth 配置項的默認值:
<?php 
// config/auth.php file
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],
'guards' => [
    ...
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
複製代碼

而後將以下內容放到你的 routes/api.php 文件裏:瀏覽器

<?php
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function () {
    Route::post('authenticate', 'AuthController@authenticate')->name('api.authenticate');
    Route::post('register', 'AuthController@register')->name('api.register');
});
複製代碼
  1. 如今咱們已經將驅動設置完成了,如法炮製,去設置你的用戶模型:
<?php
...
class User extends Authenticatable implements JWTSubject
{
    ...
     //獲取將被存儲在 JWT 主體 claim 中的標識
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    // 返回一個鍵值對數組,包含要添加到 JWT 的任何自定義 claim
    public function getJWTCustomClaims()
    {
        return [];
    }
}
複製代碼

咱們所須要作的就是實現 JWTSubject 接口而後添加相應的方法便可。

  1. 接下來,咱們須要增長權限認證方法到控制器中.

運行 php artisan make:controller AuthController 而且添加如下方法:

<?php
...
class AuthController extends Controller
{
    
    public function authenticate(Request $request){
        // 驗證字段
        $this->validate($request,['email' => 'required|email','password'=> 'required']);
        // 測試驗證
        $credentials = $request->only(['email','password']);
        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Incorrect credentials'], 401);
        }
        return response()->json(compact('token'));
    }
    public function register(Request $request){
        // 表達驗證
        $this->validate($request,[
            'email' => 'required|email|max:255|unique:users',
            'name' => 'required|max:255',
            'password' => 'required|min:8|confirmed',
        ]);
        // 建立用戶並生成 Token
        $user =  User::create([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
            'password' => Hash::make($request->input('password')),
        ]);
        $token = JWTAuth::fromUser($user);
        return response()->json(compact('token'));
    }
}
複製代碼

這一步很是直接,咱們要作的就是添加 authenticateregister 方法到咱們的控制器中。在 authenticate 方法,咱們驗證了輸入,嘗試去登陸,若是成功就返回令牌。在 register 方法,咱們驗證輸入,而後基於此建立一個用戶而且生成令牌。

4. 接下來,咱們進入相對簡單的部分。 測試咱們剛寫入的內容。 使用 php artisan make:test AuthTest 生成測試類。 在新的 tests / Feature / AuthTest 中添加如下方法:

<?php 
/**
 * @test 
 * Test registration
 */
public function testRegister(){
    //建立測試用戶數據
    $data = [
        'email' => 'test@gmail.com',
        'name' => 'Test',
        'password' => 'secret1234',
        'password_confirmation' => 'secret1234',
    ];
    //發送 post 請求
    $response = $this->json('POST',route('api.register'),$data);
    //判斷是否發送成功
    $response->assertStatus(200);
    //接收咱們獲得的 token
    $this->assertArrayHasKey('token',$response->json());
    //刪除數據
    User::where('email','test@gmail.com')->delete();
}
/**
 * @test
 * Test login
 */
public function testLogin()
{
    //建立用戶
    User::create([
        'name' => 'test',
        'email'=>'test@gmail.com',
        'password' => bcrypt('secret1234')
    ]);
    //模擬登錄
    $response = $this->json('POST',route('api.authenticate'),[
        'email' => 'test@gmail.com',
        'password' => 'secret1234',
    ]);
    //判斷是否登陸成功而且收到了 token 
    $response->assertStatus(200);
    $this->assertArrayHasKey('token',$response->json());
    //刪除用戶
    User::where('email','test@gmail.com')->delete();
}
複製代碼

上面代碼中的幾行註釋歸納了代碼的大概做用。 您應該注意的一件事是咱們如何在每一個測試中建立和刪除用戶。 測試的所有要點是它們應該彼此獨立而且應該在你的理想狀況下存在數據庫中的狀態。

若是你想全局安裝它,能夠運行 $ vendor / bin / phpunit$ phpunit 命令。 運行後它應該會給你返回是否安裝成功的數據。 若是不是這種狀況,您能夠瀏覽日誌,修復並從新測試。 這就是 TDD 的美麗之處。

5. 對於本教程,咱們將使用『菜譜 Recipes』做爲咱們的 CRUD 數據模型。

首先建立咱們的遷移數據表 php artisan make:migration create_recipes_table 並添加如下內容:

<?php 
...
public function up()
{
    Schema::create('recipes', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->text('procedure')->nullable();
        $table->tinyInteger('publisher_id')->nullable();
        $table->timestamps();
    });
}
public function down()
{
    Schema::dropIfExists('recipes');
}
複製代碼

而後運行數據遷移。 如今使用命令 php artisan make:model Recipe 來生成模型並將其添加到咱們的模型中。

<?php 
...
protected $fillable = ['title','procedure'];
/**
 * 發佈者
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function publisher(){
    return $this->belongsTo(User::class);
}
複製代碼

而後將此方法添加到 user 模型。

<?php
...
  /**
 * 獲取全部菜譜
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function recipes(){
    return $this->hasMany(Recipe::class);
}
複製代碼

6. 如今咱們須要最後一部分設置來完成咱們的食譜管理。 首先,咱們將建立控制器 php artisan make:controller RecipeController 。 接下來,編輯 routes / api.php 文件並添加 create 路由端點。

<?php 
...
  Route::group(['middleware' => ['api','auth'],'prefix' => 'recipe'],function (){
    Route::post('create','RecipeController@create')->name('recipe.create');
});
複製代碼

在控制器中,還要添加 create 方法

<?php 
...
  public function create(Request $request){
    //驗證數據
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //建立配方並附加到用戶
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //返回 json 格式的食譜數據
    return $recipe->toJson();
}
複製代碼

使用 php artisan make:test RecipeTest 生成特徵測試並編輯內容,以下所示:

<?php 
...
class RecipeTest extends TestCase
{
    use RefreshDatabase;
    ...
    //建立用戶並驗證用戶身份
    protected function authenticate(){
        $user = User::create([
            'name' => 'test',
            'email' => 'test@gmail.com',
            'password' => Hash::make('secret1234'),
        ]);
        $token = JWTAuth::fromUser($user);
        return $token;
    }
  
    public function testCreate()
    {
        //獲取 token
        $token = $this->authenticate();
        $response = $this->withHeaders([
            'Authorization' => 'Bearer '. $token,
        ])->json('POST',route('recipe.create'),[
            'title' => 'Jollof Rice',
            'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
        ]);
        $response->assertStatus(200);
    }
}
複製代碼

上面的代碼你可能仍是不太理解。咱們所作的就是建立一個用於處理用戶註冊和 token 生成的方法,而後在 testCreate() 方法中使用該 token 。注意使用 RefreshDatabase trait ,這個 trait 是 Laravel 在每次測試後重置數據庫的便捷方式,很是適合咱們漂亮的小項目。

好的,因此如今,咱們只要判斷當前請求是不是響應狀態,而後繼續運行 $ vendor/bin/phpunit

若是一切運行順利,您應該收到錯誤。 ?

There was 1 failure:

  1. Tests\Feature\RecipeTest::testCreate Expected status code 200 but received 500. Failed asserting that false is true.

/home/user/sites/tdd-journey/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133 /home/user/sites/tdd-journey/tests/Feature/RecipeTest.php:49

FAILURES! Tests: 3, Assertions: 5, Failures: 1.

查看日誌文件,咱們能夠看到罪魁禍首是 RecipeUser 類中的 publisherrecipes 的關係。 Laravel 嘗試在表中找到一個字段爲 user_id 的列並將其用做於外鍵,但在咱們的遷移中,咱們將publisher_id設置爲外鍵。 如今,將行調整爲:

//食譜文件
public function publisher(){
    return $this->belongsTo(User::class,'publisher_id');
}
//用戶文件
public function recipes(){
    return $this->hasMany(Recipe::class,'publisher_id');
}
複製代碼

而後從新運行測試。 若是一切順利,咱們將得到全部綠色測試!?

...                                                                 
3 / 3 (100%)
...
OK (3 tests, 5 assertions)
複製代碼

如今咱們仍然須要測試建立配方的方法。爲此,咱們能夠判斷用戶的『菜譜 Recipes』計數。更新你的 testCreate 方法,以下所示:

<?php
...
//獲取 token
$token = $this->authenticate();
$response = $this->withHeaders([
    'Authorization' => 'Bearer '. $token,
])->json('POST',route('recipe.create'),[
    'title' => 'Jollof Rice',
    'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
]);
$response->assertStatus(200);
//獲得計數作出判斷
$count = User::where('email','test@gmail.com')->first()->recipes()->count();
$this->assertEquals(1,$count);
複製代碼

咱們如今能夠繼續編寫其他的方法。首先,編寫咱們的 routes/api.php

<?php
...
Route::group(['middleware' => ['api','auth'],'prefix' => 'recipe'],function (){
    Route::post('create','RecipeController@create')->name('recipe.create');
    Route::get('all','RecipeController@all')->name('recipe.all');
    Route::post('update/{recipe}','RecipeController@update')->name('recipe.update');
    Route::get('show/{recipe}','RecipeController@show')->name('recipe.show');
    Route::post('delete/{recipe}','RecipeController@delete')->name('recipe.delete');
});
複製代碼

接下來,咱們將方法添加到控制器。 如下面這種方式更新 RecipeController 類。

<?php 
....
//建立配方
public function create(Request $request){
    //驗證
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //建立配方並附加到用戶
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //返回配方的 json 格式數據
    return $recipe->toJson();
}
//獲取全部的配方
public function all(){
    return Auth::user()->recipes;
}
//更新配方
public function update(Request $request, Recipe $recipe){
    //檢查用戶是不是配方的全部者
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    //更新並返回
    $recipe->update($request->only('title','procedure'));
    return $recipe->toJson();
}
//顯示單個食譜的詳細信息
public function show(Recipe $recipe){
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    return $recipe->toJson();
}
//刪除一個配方
public function delete(Recipe $recipe){
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    $recipe->delete();
}
複製代碼

代碼和註釋已經很好地解釋了這個邏輯。

最後咱們的 test/Feature/RecipeTest:

<?php
...
  use RefreshDatabase;
protected $user;
// 建立用戶並驗證他
protected function authenticate(){
    $user = User::create([
        'name' => 'test',
        'email' => 'test@gmail.com',
        'password' => Hash::make('secret1234'),
    ]);
    $this->user = $user;
    $token = JWTAuth::fromUser($user);
    return $token;
}
// 測試建立路由
public function testCreate()
{
    // 獲取令牌
    $token = $this->authenticate();
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.create'),[
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $response->assertStatus(200);
    // 獲取計數並斷言
    $count = $this->user->recipes()->count();
    $this->assertEquals(1,$count);
}
// 測試顯示全部路由
public function testAll(){
    // 驗證並將配方附加到用戶
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    // 調用路由並斷言響應
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('GET',route('recipe.all'));
    $response->assertStatus(200);
    // 斷言計數爲1,第一項的標題相關
    $this->assertEquals(1,count($response->json()));
    $this->assertEquals('Jollof Rice',$response->json()[0]['title']);
}
// 測試更新路由
public function testUpdate(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    // 調用路由並斷言響應
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.update',['recipe' => $recipe->id]),[
        'title' => 'Rice',
    ]);
    $response->assertStatus(200);
    // 斷言標題是新標題
    $this->assertEquals('Rice',$this->user->recipes()->first()->title);
}
// 測試單一的展現路由
public function testShow(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('GET',route('recipe.show',['recipe' => $recipe->id]));
    $response->assertStatus(200);
    // 斷言標題是正確的
    $this->assertEquals('Jollof Rice',$response->json()['title']);
}
// 測試刪除路由
public function testDelete(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.delete',['recipe' => $recipe->id]));
    $response->assertStatus(200);
    // 斷言沒有食譜
    $this->assertEquals(0,$this->user->recipes()->count());
}
複製代碼

除了附加測試以外,咱們還添加了一個類範圍的 $user 屬性。 這樣,咱們不止能夠利用 $user 來使用 authenticate 方法不只生成令牌,並且還爲後續其餘對 $user 的操做作好了準備。

如今運行 $ vendor/bin/phpunit 若是操做正確,你應該進行全部綠色測試。

結論

但願這能讓你深度瞭解在 TDD 在 Laravel 項目中的運行方式。 他絕對是一個比這更寬泛的概念,一個不受特意方法約束的概念。

雖然這種開發方法看起來比常見的調試後期程序要耗時, 但他很適合在代碼中儘早捕獲錯誤。雖然有些狀況下非 TDD 方式會更有用,但習慣於 TDD 模式開發是一種可靠的技能和習慣。

本演練的所有代碼可參見 Github here 倉庫。請隨意使用。

乾杯!

文章轉自:https://learnku.com/laravel/t/22690 更多文章:https://learnku.com/laravel/c/translations

相關文章
相關標籤/搜索