使用 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

相关文章
相关标签/搜索