文章轉發自專業的Laravel開發者社區,原始連接:learnku.com/laravel/t/3…php
這是 TDD 和敏捷開發方法學的先驅之一 James Grenning 的名言html
若是您不進行測試驅動的開發,那麼您將進行後期調試 - James Grenninglaravel
今天咱們將進行測試驅動的 Laravel 之旅。咱們將建立具備身份驗證和 CRUD 功能的 Laravel REST API,而無需打開 Postman 或者瀏覽器。 😲git
讓我們從創建一個新的 Laravel 專案開始 composer create-project --prefer-dist laravel/laravel tdd-journey
。數據庫
下一步,我們須要運行建構用戶認證的指令,我們將在後面用到它,繼續運行 php artisan make:auth
,接著 php artisan migrate
。json
我們不是真的會用到生成的路由及視圖。在這個項目裡,我們會使用 jwt-auth。因此繼續在你的應用裡 配置它。api
注意: 若是你在使用 JWT 的
generate
指令時碰到錯誤,你能夠依照 這裡 的指示來修復,直到它被加入下一個穩定版。瀏覽器
最後,您能夠刪除 tests/Unit
和 tests/Feature
文件夾中的 ExampleTest
,確保它不會干擾咱們的測試結果,而後咱們繼續。bash
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');
});
複製代碼
<?php
...
class User extends Authenticatable implements JWTSubject
{
...
// 取得會被儲存在 JWT 物件中的 ID
public function getJWTIdentifier()
{
return $this->getKey();
}
// 返回一個包含全部客製化參數的鍵值組,此鍵值組會被加入 JWT 中
public function getJWTCustomClaims()
{
return [];
}
}
複製代碼
我們所作的是實做 JWTSubject
並加入必要的方法。
運行 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'));
}
}
複製代碼
這一步很是直接,咱們所作的只是將 authenticate
和 register
方法添加到控制器中。在 authenticate
方法中,咱們驗證輸入的字段,而後嘗試登陸並驗證登陸信息,若是成功則返回令牌token。在register方法中,咱們驗證輸入的字段,用輸入的信息建立一個新用戶,並基於該用戶生成一個令牌,並給用戶返回該令牌。
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的美妙週期。
首先讓咱們運行遷移命令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
建立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);
}
複製代碼
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. 你能夠隨意擺弄它。
謝謝!