如何經過測試驅動開發構建 Laravel REST API

Laravel

文章轉發自專業的Laravel開發者社區,原始連接:learnku.com/laravel/t/3…php

這是 TDD 和敏捷開發方法學的先驅之一  James Grenning 的名言html

若是您不進行測試驅動的開發,那麼您將進行後期調試 - James Grenninglaravel

今天咱們將進行測試驅動的 Laravel 之旅。咱們將建立具備身份驗證和 CRUD 功能的 Laravel REST API,而無需打開 Postman 或者瀏覽器。 😲git

注意: 本旅程假定你理解 Laravel 和 PHPUnit 的基礎概念。若是你不打算這麼作?開車吧。github

配置專案

讓我們從創建一個新的 Laravel 專案開始 composer create-project --prefer-dist laravel/laravel tdd-journey數據庫

下一步,我們須要運行建構用戶認證的指令,我們將在後面用到它,繼續運行 php artisan make:auth,接著 php artisan migratejson

我們不是真的會用到生成的路由及視圖。在這個項目裡,我們會使用 jwt-auth。因此繼續在你的應用裡 配置它api

注意: 若是你在使用 JWT 的 generate 指令時碰到錯誤,你能夠依照 這裡 的指示來修復,直到它被加入下一個穩定版。瀏覽器

最後,您能夠刪除 tests/Unittests/Feature 文件夾中的 ExampleTest,確保它不會干擾咱們的測試結果,而後咱們繼續。bash

編寫代碼

  1. 首先將您的 auth 配置爲默認使用 JWT 爲驅動程序:
<?php 
// config/auth.php 文件
'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. 現在我們的驅動已經配置好了,接著配置你的 User 模型:
<?php
...
class User extends Authenticatable implements JWTSubject
{
    ...
     // 取得會被儲存在 JWT 物件中的 ID
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    // 返回一個包含全部客製化參數的鍵值組,此鍵值組會被加入 JWT 中
    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 方法中,咱們驗證輸入的字段,而後嘗試登陸並驗證登陸信息,若是成功則返回令牌token。在register方法中,咱們驗證輸入的字段,用輸入的信息建立一個新用戶,並基於該用戶生成一個令牌,並給用戶返回該令牌。

  1. 接下來,測試咱們剛剛寫好的部分。使用 php artisan make:test AuthTest命令建立一個測試類。在新的 tests/Feature/AuthTest 文件中添加下面這些代碼:
<?php 
/**
 * @test 
 * 測試註冊
 */
public function testRegister(){
    //User的數據
    $data = [
        'email' => 'test@gmail.com',
        'name' => 'Test',
        'password' => 'secret1234',
        'password_confirmation' => 'secret1234',
    ];
    //發送 post 請求
    $response = $this->json('POST',route('api.register'),$data);
    //斷言他是成功的
    $response->assertStatus(200);
    //斷言咱們收到了令牌
    $this->assertArrayHasKey('token',$response->json());
    //刪除數據
    User::where('email','test@gmail.com')->delete();
}
/**
 * @test
 * 測試成功
 */
public function testLogin()
{
    //建立 user
    User::create([
        'name' => 'test',
        'email'=>'test@gmail.com',
        'password' => bcrypt('secret1234')
    ]);
    //嘗試登錄
    $response = $this->json('POST',route('api.authenticate'),[
        'email' => 'test@gmail.com',
        'password' => 'secret1234',
    ]);
    //斷言它成功而且收到了令牌
    $response->assertStatus(200);
    $this->assertArrayHasKey('token',$response->json());
    //刪除user數據
    User::where('email','test@gmail.com')->delete();
}
複製代碼

經過上面的代碼註釋咱們能很好的理解代碼表達的意思。您應該注意的一件事是,咱們如何在每一個測試中建立和刪除用戶。咱們須要注意的是每次測試都是單獨的,丙炔數據庫的狀態是完美的。

如今讓咱們運行$vendor/bin/phpunit$phpunit(若是您在全局安裝過它)。運行它你會獲得成功結果。若是不是這樣,您能夠查看日誌,修復並從新測試。這是TDD的美妙週期。

  1. 如今咱們已經能夠進行身份驗證了,讓咱們爲項目添加CURD(數據庫的基本操做增刪改查)。 在本教程中,咱們將使用食物食譜做爲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');
}

複製代碼

gist.github.com/kofoworola/…

而後運行遷移。如今使用命令 php artisan make:model Recipe建立Recipe模型而且添加下面代碼到咱們的模型中。

<?php 
...
protected $fillable = ['title','procedure'];
/**
 * 創建與User模型的關係
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function publisher(){
    return $this->belongsTo(User::class);
}
複製代碼

而後添加下面代碼到 user 模型。

<?php
...
  /**
 * 獲取全部的recipes(一對多)
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function recipes(){
    return $this->hasMany(Recipe::class);
}
複製代碼
  1. 如今咱們須要使用路由來管理咱們的食譜。首先,咱們使用命令 php artisan make:controller RecipeController建立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){
    //Validate
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //建立 recipe 並關聯到 user
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //返回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:49FAILURES!
Tests: 3, Assertions: 5, Failures: 1.

複製代碼

查看日誌文件, 咱們能夠看到罪魁禍首是在「食譜」和「用戶」類中的「發佈者」和「食譜」的關係。laravel努力在表中尋找user_id列 ,而且使用它做爲外鍵, 可是在咱們的遷移中咱們設置publisher_id做爲外鍵. 如今咱們根據如下內容調整行:

**//Recipe file\
public function** publisher(){\
    **return** $this->belongsTo(User::**class**,'publisher_id');\
}//User file\
**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 
....
//建立 recipe
public function create(Request $request){
    //Validate
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //Create recipe and attach to user
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //Return json of recipe
    return $recipe->toJson();
}
//獲取全部的recipes
public function all(){
    return Auth::user()->recipes;
}
//更新recipe
public function update(Request $request, Recipe $recipe){
    //檢查更新這是不是recipe的全部者
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    //更新並返回
    $recipe->update($request->only('title','procedure'));
    return $recipe->toJson();
}
//展現recipe的詳情
public function show(Recipe $recipe){
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    return $recipe->toJson();
}
//刪除recipe
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
    $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);
    // 斷言響應內容只有一項,而且第一項的標題是 Jollof Rice
    $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());
}
複製代碼

除了附加的測試以外,唯一不一樣的是添加了一個類範圍的用戶文件。這樣,authenticate 方法不只生成令牌,並且還爲後續操做設置用戶文件。

如今運行 $ vendor/bin/phpunit ,若是作的都正確的話,你應該能收到一個綠色的測試經過的提示。

總結

但願這能讓您深刻了解 TDD 在 Laravel 中是如何工做的。固然,它絕對是一個比這更普遍的概念,不受特定方法的約束。

儘管這種開發方式看起來比一般的 後期調試 過程要長,但它很是適合在早期捕獲你代碼中的錯誤。儘管在某些狀況下,非 TDD 方法可能更有用,但它仍然是一種須要習慣的可靠技能和素養。

本演練的完整代碼能夠在Github上找到 here. 你能夠隨意擺弄它。

謝謝!

相關文章
相關標籤/搜索