热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

使用TDD测试驱动开发来构建LaravelRESTAPI

TDD以及敏捷开发的先驱者之一的 JamesGrenning有句名言:如果你没有进行测试驱动开发,那么你应该正在做开发后堵漏的事–James Grenning今天我们将进行一场基于

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

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

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

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

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

项目设置

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

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

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

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

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

编码

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


// config/auth.php file
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
...
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

然后将如下内容放到你的 routes/api.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. 现在我们已经将驱动设置完成了,如法炮制,去设置你的用户模型:

...
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 并且添加以下方法:

...
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 中添加以下方法:

/**
* @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 并添加以下内容:


...
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 来生成模型并将其添加到我们的模型中。

...
protected $fillable = ['title','procedure'];
/**
* 发布者
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function publisher(){
return $this->belongsTo(User::class);
}

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

...
/**
* 获取所有菜谱
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function recipes(){
return $this->hasMany(Recipe::class);
}

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

...
Route::group(['middleware' => ['api','auth'],'prefix' => 'recipe'],function (){
Route::post('create','RecipeController@create')->name('recipe.create');
});

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

...
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 生成特征测试并编辑内容,如下所示:

...
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) TestsFeatureRecipeTest::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 方法,如下所示:


...
//获取 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

...
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 类。

....
//创建配方
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:

...
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…

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


推荐阅读
  • postman参数配置
    一、postman概述1.1定义1、postman:一款功能强大的网页调试与发送网页HTTP请求的工具。2、测试集合collection:collection可以看作是reques ... [详细]
  • 如何调试php网站,如何调试php网站
    常用调试方式通过浏览器打印信息进行调试方法在代码中添加echo、var_dump、print_r和exit,在浏览器中查看输出。优缺点优点:简单 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • PDO MySQL
    PDOMySQL如果文章有成千上万篇,该怎样保存?数据保存有多种方式,比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择,做We ... [详细]
  • syncd是一款开源的代码部署工具,它具有简单、高效、易用等特点,可以提高团队的工作效率. ... [详细]
  • SAP接口编程PyRFC 调用 BAPI_FIXEDASSET_CREATE1创建固定资产
    本篇演示通过PyRFC调用BAPI_FIXEDASSET_CREATE1在SAP系统中创建固定资产,再一次体验一下Python与其它语言相比的简洁性。首先简单说明B ... [详细]
  • Python爬取小姐姐内衣信息,寻找妹纸们的偏好
    今天继续来分析爬虫数据分析文章,一起来看看网易严选商品评论的获取和分析。警告:本教程仅用作学习交流,请勿用作商业盈利,违者后果自负!如本文有侵犯任何组织集团 ... [详细]
author-avatar
和谐啄木鸟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有