文章转发自专业的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. 你能够随意摆弄它。
谢谢!