美文网首页
如何通过测试驱动开发构建 Laravel REST API

如何通过测试驱动开发构建 Laravel REST API

作者: summerbluet | 来源:发表于2019-12-17 10:00 被阅读0次
    Laravel

    文章转发自专业的Laravel开发者社区,原始链接:https://learnku.com/laravel/t/37442

    这是 TDD 和敏捷开发方法学的先驱之一 James Grenning 的名言

    如果您不进行测试驱动的开发,那么您将进行后期调试 - James Grenning

    今天我们将进行测试驱动的 Laravel 之旅。我们将创建具有身份验证和 CRUD 功能的 Laravel REST API,而无需打开 Postman 或者浏览器。 😲

    注意: 本旅程假定你理解 LaravelPHPUnit 的基础概念。如果你不打算这么做?开车吧。

    配置專案

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

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

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

    注意: 如果你在使用 JWT 的 generate 指令時碰到錯誤,你可以依照 這裡 的指示來修復,直到它被加入下一個穩定版。

    最后,您可以删除 tests/Unittests/Feature 文件夹中的 ExampleTest,确保它不会干扰我们的测试结果,然后我们继续。

    编写代码

    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');
    }
    
    

    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);
    }
    
    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. 你可以随意摆弄它。

    谢谢!


    相关文章

      网友评论

          本文标题:如何通过测试驱动开发构建 Laravel REST API

          本文链接:https://www.haomeiwen.com/subject/vlxsnctx.html