文章转发自专业的Laravel开发者社区,原始链接:https://learnku.com/laravel/t/37442
这是 TDD 和敏捷开发方法学的先驱之一 James Grenning 的名言
如果您不进行测试驱动的开发,那么您将进行后期调试 - James Grenning
今天我们将进行测试驱动的 Laravel 之旅。我们将创建具有身份验证和 CRUD 功能的 Laravel REST API,而无需打开 Postman 或者浏览器。 😲
配置專案
讓我們從建立一個新的 Laravel 專案開始 composer create-project --prefer-dist laravel/laravel tdd-journey
。
下一步,我們需要運行建構用戶認證的指令,我們將在後面用到它,繼續運行 php artisan make:auth
,接著 php artisan migrate
。
我們不是真的會用到生成的路由及視圖。在這個項目裡,我們會使用 jwt-auth。所以繼續在你的應用裡 配置它。
注意: 如果你在使用 JWT 的
generate
指令時碰到錯誤,你可以依照 這裡 的指示來修復,直到它被加入下一個穩定版。
最后,您可以删除 tests/Unit
和 tests/Feature
文件夹中的 ExampleTest
,确保它不会干扰我们的测试结果,然后我们继续。
编写代码
- 首先将您的
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');
});
- 現在我們的驅動已經配置好了,接著配置你的 User 模型:
<?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的美妙周期。
- 现在我们已经可以进行身份验证了,让我们为项目添加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');
}
https://gist.github.com/kofoworola/14fd5031f9e2733ebe4d3de5f1ae0490
然后运行迁移。现在使用命令 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. 你可以随意摆弄它。
谢谢!
网友评论