资讯专栏INFORMATION COLUMN

使用 TDD 测试驱动开发来构建 Laravel REST API

dackel / 1617人阅读

摘要:以及敏捷开发的先驱者之一的有句名言如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事今天我们将进行一场基于的测试驱动开发之旅。使用生成测试类。现在使用命令来生成模型并将其添加到我们的模型中。

TDD 以及敏捷开发的先驱者之一的 James Grenning有句名言:

</>复制代码

  1. 如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事 - James Grenning

今天我们将进行一场基于 Laravel 的测试驱动开发之旅。 我们将创建一个完整的 Laravel REST API,其中包含身份验证和 CRUD 功能,而无需打开 Postman 或浏览器。?

</>复制代码

  1. 注意:本旅程假定你已经理解了 Laravel 和 PHPUnit 的基本概念。你是否已经明晰了这个问题?那就开始吧。

项目设置

首先创建一个新的 Laravel 项目 composer create-project --prefer-dist laravel/laravel tdd-journey

然后,我们需要创建 用户认证 脚手架,执行  php artisan make:auth ,设置好 .env 文件中的数据库配置后,执行数据库迁移 php artisan migrate

本测试项目并不会使用到刚生成的用户认证相关的路由和视图。我们将使用 jwt-auth。所以需要继续 安装 jwt 到项目。

</>复制代码

  1. 注意:如果您在执行 jwt:generate 指令时遇到错误, 您可以参考 这里解决这个问题,直到 jwt 被正确安装到项目中。

最后,您需要在 tests/Unittests/Feature 目录中删除 ExampleTest.php 文件,使我们的测试结果不被影响。

编码

首先将 JWT 驱动配置为 auth 配置项的默认值:

</>复制代码

  1. [
  2. "guard" => "api",
  3. "passwords" => "users",
  4. ],
  5. "guards" => [
  6. ...
  7. "api" => [
  8. "driver" => "jwt",
  9. "provider" => "users",
  10. ],
  11. ],

然后将如下内容放到你的 routes/api.php 文件里:

</>复制代码

  1. "api", "prefix" => "auth"], function () {
  2. Route::post("authenticate", "AuthController@authenticate")->name("api.authenticate");
  3. Route::post("register", "AuthController@register")->name("api.register");
  4. });

现在我们已经将驱动设置完成了,如法炮制,去设置你的用户模型:

</>复制代码

  1. getKey();
  2. }
  3. // 返回一个键值对数组,包含要添加到 JWT 的任何自定义 claim
  4. public function getJWTCustomClaims()
  5. {
  6. return [];
  7. }
  8. }

我们所需要做的就是实现 JWTSubject 接口然后添加相应的方法即可。

接下来,我们需要增加权限认证方法到控制器中.

运行 php artisan make:controller AuthController 并且添加以下方法:

</>复制代码

  1. validate($request,["email" => "required|email","password"=> "required"]);
  2. // 测试验证
  3. $credentials = $request->only(["email","password"]);
  4. if (! $token = auth()->attempt($credentials)) {
  5. return response()->json(["error" => "Incorrect credentials"], 401);
  6. }
  7. return response()->json(compact("token"));
  8. }
  9. public function register(Request $request){
  10. // 表达验证
  11. $this->validate($request,[
  12. "email" => "required|email|max:255|unique:users",
  13. "name" => "required|max:255",
  14. "password" => "required|min:8|confirmed",
  15. ]);
  16. // 创建用户并生成 Token
  17. $user = User::create([
  18. "name" => $request->input("name"),
  19. "email" => $request->input("email"),
  20. "password" => Hash::make($request->input("password")),
  21. ]);
  22. $token = JWTAuth::fromUser($user);
  23. return response()->json(compact("token"));
  24. }
  25. }

这一步非常直接,我们要做的就是添加 authenticateregister 方法到我们的控制器中。在 authenticate 方法,我们验证了输入,尝试去登录,如果成功就返回令牌。在 register 方法,我们验证输入,然后基于此创建一个用户并且生成令牌。

4. 接下来,我们进入相对简单的部分。 测试我们刚写入的内容。 使用 php artisan make:test AuthTest 生成测试类。 在新的 tests / Feature / AuthTest 中添加以下方法:

</>复制代码

  1. "test@gmail.com",
  2. "name" => "Test",
  3. "password" => "secret1234",
  4. "password_confirmation" => "secret1234",
  5. ];
  6. //发送 post 请求
  7. $response = $this->json("POST",route("api.register"),$data);
  8. //判断是否发送成功
  9. $response->assertStatus(200);
  10. //接收我们得到的 token
  11. $this->assertArrayHasKey("token",$response->json());
  12. //删除数据
  13. User::where("email","test@gmail.com")->delete();
  14. }
  15. /**
  16. * @test
  17. * Test login
  18. */
  19. public function testLogin()
  20. {
  21. //创建用户
  22. User::create([
  23. "name" => "test",
  24. "email"=>"test@gmail.com",
  25. "password" => bcrypt("secret1234")
  26. ]);
  27. //模拟登陆
  28. $response = $this->json("POST",route("api.authenticate"),[
  29. "email" => "test@gmail.com",
  30. "password" => "secret1234",
  31. ]);
  32. //判断是否登录成功并且收到了 token
  33. $response->assertStatus(200);
  34. $this->assertArrayHasKey("token",$response->json());
  35. //删除用户
  36. User::where("email","test@gmail.com")->delete();
  37. }

上面代码中的几行注释概括了代码的大概作用。 您应该注意的一件事是我们如何在每个测试中创建和删除用户。 测试的全部要点是它们应该彼此独立并且应该在你的理想情况下存在数据库中的状态。

如果你想全局安装它,可以运行 $ vendor / bin / phpunit$ phpunit 命令。 运行后它应该会给你返回是否安装成功的数据。 如果不是这种情况,您可以浏览日志,修复并重新测试。 这就是 TDD 的美丽之处。

5. 对于本教程,我们将使用『菜谱 Recipes』作为我们的 CRUD 数据模型。

首先创建我们的迁移数据表 php artisan make:migration create_recipes_table 并添加以下内容:

</>复制代码

  1. increments("id");
  2. $table->string("title");
  3. $table->text("procedure")->nullable();
  4. $table->tinyInteger("publisher_id")->nullable();
  5. $table->timestamps();
  6. });
  7. }
  8. public function down()
  9. {
  10. Schema::dropIfExists("recipes");
  11. }

然后运行数据迁移。 现在使用命令 php artisan make:model Recipe 来生成模型并将其添加到我们的模型中。

</>复制代码

  1. belongsTo(User::class);
  2. }

然后将此方法添加到 user 模型。

</>复制代码

  1. hasMany(Recipe::class);
  2. }

6. 现在我们需要最后一部分设置来完成我们的食谱管理。 首先,我们将创建控制器 php artisan make:controller RecipeController 。 接下来,编辑 routes / api.php 文件并添加 create 路由端点。

</>复制代码

  1. ["api","auth"],"prefix" => "recipe"],function (){
  2. Route::post("create","RecipeController@create")->name("recipe.create");
  3. });

在控制器中,还要添加 create 方法

</>复制代码

  1. validate($request,["title" => "required","procedure" => "required|min:8"]);
  2. //创建配方并附加到用户
  3. $user = Auth::user();
  4. $recipe = Recipe::create($request->only(["title","procedure"]));
  5. $user->recipes()->save($recipe);
  6. //返回 json 格式的食谱数据
  7. return $recipe->toJson();
  8. }

使用 php artisan make:test RecipeTest 生成特征测试并编辑内容,如下所示:

</>复制代码

  1. "test",
  2. "email" => "test@gmail.com",
  3. "password" => Hash::make("secret1234"),
  4. ]);
  5. $token = JWTAuth::fromUser($user);
  6. return $token;
  7. }
  8. public function testCreate()
  9. {
  10. //获取 token
  11. $token = $this->authenticate();
  12. $response = $this->withHeaders([
  13. "Authorization" => "Bearer ". $token,
  14. ])->json("POST",route("recipe.create"),[
  15. "title" => "Jollof Rice",
  16. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  17. ]);
  18. $response->assertStatus(200);
  19. }
  20. }

上面的代码你可能还是不太理解。我们所做的就是创建一个用于处理用户注册和 token 生成的方法,然后在 testCreate() 方法中使用该 token 。注意使用 RefreshDatabase trait ,这个 trait 是 Laravel 在每次测试后重置数据库的便捷方式,非常适合我们漂亮的小项目。

好的,所以现在,我们只要判断当前请求是否是响应状态,然后继续运行 $ vendor/bin/phpunit

如果一切运行顺利,您应该收到错误。 ?

</>复制代码

  1. There was 1 failure:

    1) TestsFeatureRecipeTest::testCreate
    Expected status code 200 but received 500.
    Failed asserting that false is true.

  2. /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

  3. FAILURES!
    Tests: 3, Assertions: 5, Failures: 1.

查看日志文件,我们可以看到罪魁祸首是 RecipeUser 类中的 publisherrecipes 的关系。 Laravel 尝试在表中找到一个字段为 user_id 的列并将其用作于外键,但在我们的迁移中,我们将publisher_id 设置为外键。 现在,将行调整为:

</>复制代码

  1. //食谱文件
  2. public function publisher(){
  3. return $this->belongsTo(User::class,"publisher_id");
  4. }
  5. //用户文件
  6. public function recipes(){
  7. return $this->hasMany(Recipe::class,"publisher_id");
  8. }

然后重新运行测试。 如果一切顺利,我们将获得所有绿色测试!?

</>复制代码

  1. ...
  2. 3 / 3 (100%)
  3. ...
  4. OK (3 tests, 5 assertions)

现在我们仍然需要测试创建配方的方法。为此,我们可以判断用户的『菜谱 Recipes』计数。更新你的 testCreate 方法,如下所示:

</>复制代码

  1. authenticate();
  2. $response = $this->withHeaders([
  3. "Authorization" => "Bearer ". $token,
  4. ])->json("POST",route("recipe.create"),[
  5. "title" => "Jollof Rice",
  6. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  7. ]);
  8. $response->assertStatus(200);
  9. //得到计数做出判断
  10. $count = User::where("email","test@gmail.com")->first()->recipes()->count();
  11. $this->assertEquals(1,$count);

我们现在可以继续编写其余的方法。首先,编写我们的 routes/api.php

</>复制代码

  1. ["api","auth"],"prefix" => "recipe"],function (){
  2. Route::post("create","RecipeController@create")->name("recipe.create");
  3. Route::get("all","RecipeController@all")->name("recipe.all");
  4. Route::post("update/{recipe}","RecipeController@update")->name("recipe.update");
  5. Route::get("show/{recipe}","RecipeController@show")->name("recipe.show");
  6. Route::post("delete/{recipe}","RecipeController@delete")->name("recipe.delete");
  7. });

接下来,我们将方法添加到控制器。 以下面这种方式更新 RecipeController 类。

</>复制代码

  1. validate($request,["title" => "required","procedure" => "required|min:8"]);
  2. //创建配方并附加到用户
  3. $user = Auth::user();
  4. $recipe = Recipe::create($request->only(["title","procedure"]));
  5. $user->recipes()->save($recipe);
  6. //返回配方的 json 格式数据
  7. return $recipe->toJson();
  8. }
  9. //获取所有的配方
  10. public function all(){
  11. return Auth::user()->recipes;
  12. }
  13. //更新配方
  14. public function update(Request $request, Recipe $recipe){
  15. //检查用户是否是配方的所有者
  16. if($recipe->publisher_id != Auth::id()){
  17. abort(404);
  18. return;
  19. }
  20. //更新并返回
  21. $recipe->update($request->only("title","procedure"));
  22. return $recipe->toJson();
  23. }
  24. //显示单个食谱的详细信息
  25. public function show(Recipe $recipe){
  26. if($recipe->publisher_id != Auth::id()){
  27. abort(404);
  28. return;
  29. }
  30. return $recipe->toJson();
  31. }
  32. //删除一个配方
  33. public function delete(Recipe $recipe){
  34. if($recipe->publisher_id != Auth::id()){
  35. abort(404);
  36. return;
  37. }
  38. $recipe->delete();
  39. }

代码和注释已经很好地解释了这个逻辑。

最后我们的 test/Feature/RecipeTest:

</>复制代码

  1. "test",
  2. "email" => "test@gmail.com",
  3. "password" => Hash::make("secret1234"),
  4. ]);
  5. $this->user = $user;
  6. $token = JWTAuth::fromUser($user);
  7. return $token;
  8. }
  9. // 测试创建路由
  10. public function testCreate()
  11. {
  12. // 获取令牌
  13. $token = $this->authenticate();
  14. $response = $this->withHeaders([
  15. "Authorization" => "Bearer ". $token,
  16. ])->json("POST",route("recipe.create"),[
  17. "title" => "Jollof Rice",
  18. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  19. ]);
  20. $response->assertStatus(200);
  21. // 获取计数并断言
  22. $count = $this->user->recipes()->count();
  23. $this->assertEquals(1,$count);
  24. }
  25. // 测试显示所有路由
  26. public function testAll(){
  27. // 验证并将配方附加到用户
  28. $token = $this->authenticate();
  29. $recipe = Recipe::create([
  30. "title" => "Jollof Rice",
  31. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  32. ]);
  33. $this->user->recipes()->save($recipe);
  34. // 调用路由并断言响应
  35. $response = $this->withHeaders([
  36. "Authorization" => "Bearer ". $token,
  37. ])->json("GET",route("recipe.all"));
  38. $response->assertStatus(200);
  39. // 断言计数为1,第一项的标题相关
  40. $this->assertEquals(1,count($response->json()));
  41. $this->assertEquals("Jollof Rice",$response->json()[0]["title"]);
  42. }
  43. // 测试更新路由
  44. public function testUpdate(){
  45. $token = $this->authenticate();
  46. $recipe = Recipe::create([
  47. "title" => "Jollof Rice",
  48. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  49. ]);
  50. $this->user->recipes()->save($recipe);
  51. // 调用路由并断言响应
  52. $response = $this->withHeaders([
  53. "Authorization" => "Bearer ". $token,
  54. ])->json("POST",route("recipe.update",["recipe" => $recipe->id]),[
  55. "title" => "Rice",
  56. ]);
  57. $response->assertStatus(200);
  58. // 断言标题是新标题
  59. $this->assertEquals("Rice",$this->user->recipes()->first()->title);
  60. }
  61. // 测试单一的展示路由
  62. public function testShow(){
  63. $token = $this->authenticate();
  64. $recipe = Recipe::create([
  65. "title" => "Jollof Rice",
  66. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  67. ]);
  68. $this->user->recipes()->save($recipe);
  69. $response = $this->withHeaders([
  70. "Authorization" => "Bearer ". $token,
  71. ])->json("GET",route("recipe.show",["recipe" => $recipe->id]));
  72. $response->assertStatus(200);
  73. // 断言标题是正确的
  74. $this->assertEquals("Jollof Rice",$response->json()["title"]);
  75. }
  76. // 测试删除路由
  77. public function testDelete(){
  78. $token = $this->authenticate();
  79. $recipe = Recipe::create([
  80. "title" => "Jollof Rice",
  81. "procedure" => "Parboil rice, get pepper and mix, and some spice and serve!"
  82. ]);
  83. $this->user->recipes()->save($recipe);
  84. $response = $this->withHeaders([
  85. "Authorization" => "Bearer ". $token,
  86. ])->json("POST",route("recipe.delete",["recipe" => $recipe->id]));
  87. $response->assertStatus(200);
  88. // 断言没有食谱
  89. $this->assertEquals(0,$this->user->recipes()->count());
  90. }

除了附加测试之外,我们还添加了一个类范围的 $user 属性。 这样,我们不止可以利用 $user 来使用 authenticate 方法不仅生成令牌,而且还为后续其他对 $user 的操作做好了准备。

现在运行 $ vendor/bin/phpunit 如果操作正确,你应该进行所有绿色测试。

结论

希望这能让你深度了解在 TDD 在 Laravel 项目中的运行方式。 他绝对是一个比这更宽泛的概念,一个不受特地方法约束的概念。

虽然这种开发方法看起来比常见的调试后期程序要耗时, 但他很适合在代码中尽早捕获错误。虽然有些情况下非 TDD 方式会更有用,但习惯于 TDD 模式开发是一种可靠的技能和习惯。

本演练的全部代码可参见 Github here 仓库。请随意使用。

干杯!

</>复制代码

  1. 文章转自:https://learnku.com/laravel/t...

    更多文章:https://learnku.com/laravel/c...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/8920.html

相关文章

  • 使用 TDD 测试驱动开发构建 Laravel REST API

    摘要:以及敏捷开发的先驱者之一的有句名言如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事今天我们将进行一场基于的测试驱动开发之旅。使用生成测试类。现在使用命令来生成模型并将其添加到我们的模型中。 showImg(https://segmentfault.com/img/remote/1460000018404936?w=1440&h=900); TDD 以及敏捷开发的先驱者之一的 ...

    AlphaWallet 评论0 收藏0
  • 快速部署TEST-DRIVEN DEVELOPMENT/DEBUG环境

    摘要:关注的目标就是在代码提交之后,顺利且迅速的把新的功能部署到产品环境上。由于是,那么单元测试,回归测试,集成测试,都是实现的手段。高质量的产品需求书和高质量的自动化集成测试用例毫无疑问,是高质量软件的保证之一。 showImg(https://segmentfault.com/img/remote/1460000006877091?w=800&h=600); 什么是Test-Driven...

    SHERlocked93 评论0 收藏0
  • 《Python Web开发》作者Harry Percival:TDD就是微小而渐进的改变

    摘要:目前就职于,他在各种演讲研讨会和开发者大会上积极推广测试驱动开发。问很多敏捷教练都表示训练新人做测试驱动开发是一件辛苦而进度缓慢的事,并且收益也不是很大。首先是开发的对话式风格。第一个问题就是测试套件的速度。 Harry J.W. Percival目前就职于PythonAnywhere,他在各种演讲、研讨会和开发者大会上积极推广测试驱动开发(TDD)。他在利物浦大学获得计算机科学硕士学...

    Guakin_Huang 评论0 收藏0
  • 《Python Web开发》作者Harry Percival:TDD就是微小而渐进的改变

    摘要:目前就职于,他在各种演讲研讨会和开发者大会上积极推广测试驱动开发。问很多敏捷教练都表示训练新人做测试驱动开发是一件辛苦而进度缓慢的事,并且收益也不是很大。首先是开发的对话式风格。第一个问题就是测试套件的速度。 Harry J.W. Percival目前就职于PythonAnywhere,他在各种演讲、研讨会和开发者大会上积极推广测试驱动开发(TDD)。他在利物浦大学获得计算机科学硕士学...

    k00baa 评论0 收藏0
  • PHP / Laravel API 开发推荐阅读清单

    showImg(https://segmentfault.com/img/bV6aHV?w=1280&h=800); 社区优秀文章 Laravel 5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 - 自造车轮。 API 文档神器 Swagger 介绍及在 PHP 项目中使用 - API 文档撰写方案 推荐 Laravel API 项目必须使用的 8 个...

    shmily 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<