API 系列教程(一):基于 Laravel 5.5 构建 和 测试 RESTful API

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lamp_yang_3533/article/details/84723996

随着移动开发和 JavaScript 框架的日益流行,使用 RESTful API 在数据层和客户端之间构建交互接口逐渐成为最佳选择。

在本系列教程中,将会带领大家基于 Laravel 5.5 来构建并测试带认证功能的 RESTful API。

RESTful API

先要了解什么是 RESTful API。REST 是 Representational State Transfer 的缩写,表示一种应用之间网络通信的架构风格,依赖于无状态的协议(通常是HTTP)进行交互。

通过 HTTP 动词表示操作

在 RESTful API 中,我们使用 HTTP 动词表示操作,而端点是操作的资源,HTTP 动词的语义如下:

  • GET:获取资源
  • POST:创建资源
  • PUT:更新资源
  • DELETE:删除资源

更新操作:PUT vs POST

关于 RESTful API 有一些争议,比如更新资源使用 POST、PATCH 还是 PUT 哪一个更好?

在本教程中,我们使用 PUT 进行更新操作,因为基于 HTTP RFC 标准,PUT 的含义是在指定位置上创建/更新资源;使用 PUT 的另一个原因是幂等性,这意味着不管你发送一次、两次还是上千次请求,操作结果都一致。

资源

在 RESTful API 中,资源指的是操作的对象,在我们的例子中就是文章(Articles)和用户(Users)。

它们各自的端点是:

  • /articles
  • /users

在我们的教程中,资源和数据模型一一对应,但这并不是强制性的要求。

关于一致性的注意项

使用 REST 的最大好处是更容易消费和开发 API,一些端点非常直截了当,这样相较于类似 GET /get_article?id_article=12 这样的端点 RESTful API 更容易使用和维护。

不过,在某些案例中映射到 Create/Retrieve/Update/Delete 可能会很困难。

需要牢记的是: URL 中不要包含任何动词而且资源并不一定非得是数据表的某一行数据。另一个需要记住的是不必为每个资源实现所有操作。

构建一个新的 Laravel 项目

创建新应用

首先,通过 Composer 来安装 Laravel 5.5。

composer create-project --prefer-dist laravel/laravel apidemo 5.5.*

然后,自行配置 web 服务器,并修改 hosts 文件。

127.0.0.1 apidemo.test

检测是否可以正常访问 http://apidemo.test 。

修改 config/app.php 中的时区 timezone 配置:

'timezone' => 'Asia/Shanghai',

创建迁移和模型

在编写第一个迁移之前,需要将 .env 文件中的环境变量调整为开发环境中的数据库配置。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=apidemo
DB_USERNAME=root
DB_PASSWORD=root

接下来,就可以开始创建我们的第一个 Article 模型及其对应的迁移文件。

在项目根目录,运行如下 Artisan 命令一步到位:

php artisan make:model Article -m

-m 是 --migration 的缩写,告知 Artisan 在创建模型的同时创建与之对应的迁移文件。

上述命令创建的模型文件是 app/Article.php,迁移文件是 database/migrations/2018_04_03_160023_create_articles_table.php。

当然,还需要编辑该迁移文件的内容:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title', 100);
            $table->text('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
}

然后,运行命令 php artisan migrate ,就会根据迁移文件来创建对应的数据表结构了。

php artisan migrate

执行成功后,就可以看到 apidemo 数据库中自动生成了 4 张表,articles、migrations、password_resets 和 users。

当然,只有 articles 表是由我们手动生成的迁移文件来生成的。

由上述迁移文件生成的 articles 表的结构为:

CREATE TABLE `articles` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `body` text COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

修改模型

修改 Article 模型类,添加如下属性字段到 $fillable ,以便可以在 Article::create 和 Article::update 方法中使用它们。

class Article extends Model
{
    protected $fillable = ['title', 'body'];
}

数据库填充

Laravel 通过 Faker 库可以快速为我们生成格式正确的测试数据。

创建填充器类:

php artisan make:seeder ArticlesTableSeeder

填充器类默认会存放在 database/seeds 目录下。

编辑 database/seeds/ArticlesTableSeeder.php 填充器:

<?php

use Illuminate\Database\Seeder;
use App\Article;

class ArticlesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Let's truncate our existing records to start from scratch.
        Article::truncate();

        $faker = \Faker\Factory::create();

        // And now, let's create a few articles in our database:
        for ($i = 0; $i < 50; $i++) {
            Article::create([
                'title' => $faker->sentence,
                'body' => $faker->paragraph,
            ]);
        }
    }
}

然后运行填充命令:

php artisan db:seed --class=ArticlesTableSeeder

类似地,再创建用户表填充器类:

php artisan make:seeder UsersTableSeeder

修改 database/seeds/UsersTableSeeder.php 文件:

<?php

use Illuminate\Database\Seeder;
use App\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Let's clear the users table first
        User::truncate();

        $faker = \Faker\Factory::create();

        // Let's make sure everyone has the same password and
        // let's hash it before the loop, or else our seeder
        // will be too slow.
        $password = Hash::make('toptal');

        User::create([
            'name' => 'Administrator',
            'email' => '[email protected]',
            'password' => $password,
        ]);

        // And now let's generate a few dozen users for our app:
        for ($i = 0; $i < 10; $i++) {
            User::create([
                'name' => $faker->name,
                'email' => $faker->email,
                'password' => $password,
            ]);
        }
    }
}

然后,修改 database/seeds/DatabaseSeeder.php 文件:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UsersTableSeeder::class);
        $this->call(ArticlesTableSeeder::class);
    }
}

这样的话,只需运行下面的命令:

php artisan db:seed

就可以调用多个填充器来填充数据。

路由和控制器

注册路由

有了数据之后,接下来我们来为应用创建基本接口:创建、获取列表、获取单条记录、更新以及删除。

在 routes/api.php (API 接口路由文件)中,添加下面的路由配置:

<?php

use Illuminate\Http\Request;
use App\Article;

Route::get('articles', function() {
    // If the Content-Type and Accept headers are set to 'application/json',
    // this will return a JSON structure. This will be cleaned up later.
    return Article::all();
});

Route::get('articles/{id}', function($id) {
    return Article::find($id);
});

Route::post('articles', function(Request $request) {
    return Article::create($request->all);
});

Route::put('articles/{id}', function(Request $request, $id) {
    $article = Article::findOrFail($id);
    $article->update($request->all());

    return $article;
});

Route::delete('articles/{id}', function($id) {
    Article::find($id)->delete();

    return 204;
});

对于 routes/api.php 中定义的路由,访问时需要加上 /api/ 前缀,并且 API 限流中间件会自动应用到所有路由。

配置好上述路由之后,就可以访问某些 API 接口了。

比如,获取文章的单条记录 API 接口:

GET /api/articles/{id}

在 Postman 客户端中,测试该 API 接口。

GET http://apidemo.test/api/articles/1

创建控制器

接下来,我们来创建控制器,以便把路由闭包中的业务逻辑调整到控制器中。

// 创建文章控制器
php artisan make:controller ArticleController

编辑 app/Http/Controllers/ArticleController.php :

<?php

namespace App\Http\Controllers;

use App\Article;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    /**
     *  获取文章列表
     */
    public function index()
    {
        return Article::all();
    }

    /**
     *  获取文章的单条记录
     */
    public function show($id)
    {
        return Article::find($id);
    }

    /**
     *  创建新文章
     */
    public function store(Request $request)
    {
        return Article::create($request->all());
    }

    /**
     *  更新文章
     */
    public function update(Request $request, $id)
    {
        $article = Article::findOrFail($id);
        $article->update($request->all());

        return $article;
    }

    /**
     *  删除文章
     */
    public function delete(Request $request, $id)
    {
        $article = Article::findOrFail($id);
        $article->delete();

        return 204;
    }
}

然后,调整 routes/api.php 文件中的 articles 相关路由即可。

Route::get('articles', 'ArticleController@index');
Route::get('articles/{id}', 'ArticleController@show');
Route::post('articles', 'ArticleController@store');
Route::put('articles/{id}', 'ArticleController@update');
Route::delete('articles/{id}', 'ArticleController@delete');

隐式的路由模型绑定

还可以通过隐式路由模型绑定,来改写路由。

Route::get('articles', 'ArticleController@index');
Route::get('articles/{article}', 'ArticleController@show');
Route::post('articles', 'ArticleController@store');
Route::put('articles/{article}', 'ArticleController@update');
Route::delete('articles/{article}', 'ArticleController@delete');

当然,相应的,也要调整控制器代码:

class ArticleController extends Controller
{
    /**
     *  获取文章列表
     */
    public function index()
    {
        return Article::all();
    }

    /**
     *  获取文章的单条记录
     */
    public function show(Article $article)
    {
        return $article;
    }

    /**
     *  创建新文章
     */
    public function store(Request $request)
    {
        $article = Article::create($request->all());

        return response()->json($article, 201);
    }

    /**
     *  更新文章
     */
    public function update(Request $request, Article $article)
    {
        $article->update($request->all());

        return response()->json($article, 200);
    }

    /**
     *  删除文章
     */
    public function delete(Article $article)
    {
        $article->delete();

        return response()->json(null, 204);
    }
}

改用隐式路由模型绑定后,API 接口的返回结果与之前是一致的。

关于 HTTP 状态码的注意项

我们使用了 response()->json(),这可以让我们在显示返回 JSON 数据的同时发送可以被客户端解析的 HTTP 状态码。

常用的 HTTP 状态码如下:

  • 200:OK,标准的响应成功状态码
  • 201:Object created,用于 store 操作
  • 204:No content,操作执行成功,但是没有返回任何内容
  • 206:Partial content,返回部分资源时使用
  • 400:Bad request,请求验证失败
  • 401:Unauthorized,用户需要认证
  • 403:Forbidden,用户认证通过但是没有权限执行该操作
  • 404:Not found,请求资源不存在
  • 500:Internal server error,通常我们并不会显示返回这个状态码,除非程序异常中断
  • 503:Service unavailable,一般也不会显示返回该状态码,通常用于排查问题

发送 404 响应

如果你试图获取不存在的资源,会返回 404 页面。

比如:http://apidemo.test/api/articles/1000

对于 404 响应, Laravel 默认提供的是一个 html 页面响应。

如果想要将其改为返回 JSON 响应,可以修改异常处理器 app/Exceptions/Handler.php 的 render 方法。

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class Handler extends ExceptionHandler
{

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if ($exception instanceof ModelNotFoundException) {
            return response()->json([
                'error' => 'Resource not found.'
            ],404);
        }
        return parent::render($request, $exception);
    }
}

再来访问不存在的资源,返回的结果如下:

{
    "error": "Resource not found."
}

API 认证

在 Laravel 中实现 API 认证有多种方式(例如 Passport),这里我们使用一个非常简化的方式。

新增 api_token 字段

首先,需要添加 api_token 字段到 users 表。

我们通过创建新的迁移来修改 users 的表结构。

php artisan make:migration --table=users adds_api_token_to_users_table

编辑该迁移文件:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddsApiTokenToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('api_token', 60)->unique()->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['api_token']);
        });
    }
}

执行迁移命令,以便作用于数据表。

php artisan migrate

这样,users 表中就自动新增了 api_token 字段。

创建注册接口

我们使用 RegisterController 来根据注册请求返回正确的响应。尽管 Laravel 开箱提供了认证功能,但是我们还是需要对其进行调整以便返回我们想要的响应数据。

只需在 app/Http/Controllers/Auth/RegisterController.php 中实现 registered 方法即可。

protected function registered(Request $request, $user)
{
    $user->generateToken();

    return response()->json(['data' => $user->toArray()], 201);
}

在上面的示例代码中,我们调用了 User 模型上的生成令牌方法 generateToken(),但该方法还不存在。故我们需要修改 User 模型类(app/User.php),添加该方法。

public function generateToken()
{
   $this->api_token = str_random(60);
   $this->save();

   return $this->api_token;
}

然后,在 routes/api.php 中,添加用户注册接口的路由:

Route::post('register', 'Auth\RegisterController@register');

至此,注册接口编写完成,用户现在可以通过注册接口进行注册了,感谢 Laravel 开箱提供的认证字段验证功能,如果你需要调整验证规则的话可以到 RegisterController 中查看 validator 方法。

接下来,下面我们来简单测试下用户注册接口:

curl -X POST http://apidemo.test/api/register \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{"name": "user01", "email": "[email protected]", "password": "test123", "password_confirmation": "test123"}'

用户注册成功后的响应结果:

{
    "data": {
        "name": "user01",
        "email": "[email protected]",
        "updated_at": "2018-04-04 22:05:43",
        "created_at": "2018-04-04 22:05:43",
        "id": 14,
        "api_token": "Lqd7HfpFaptghJxyV7VaQwUv5JYqIUTehzOblvDDZxIx0M4PpIfODNcKNSVK"
    }
}

创建登录接口

和用户注册接口类似,可以编辑 LoginController 控制器来支持 API 认证。

为此,我们需要在 LoginController 覆盖 AuthenticatesUsers trait 提供的 login 方法。

修改 app/Http/Controllers/Auth/LoginController.php 文件。

先添加

use Illuminate\Http\Request;

然后,添加

public function login(Request $request)
{
    $this->validateLogin($request);

    if ($this->attemptLogin($request)) {
        $user = $this->guard()->user();
        $user->generateToken();

        return response()->json([
            'data' => $user->toArray(),
        ]);
    }

    return $this->sendFailedLoginResponse($request);
}

然后在 routes/api.php 中,配置用户登录的路由:

Route::post('login', 'Auth\LoginController@login');

现在,基于我们上面注册的新用户,我们来测试下登录接口:

curl -X POST http://apidemo.test/api/login \
-H "Content-type: application/json" \
-d '{"email": "[email protected]", "password": "test123"}'

登录成功返回结果:

{
    "data": {
        "id": 14,
        "name": "user01",
        "email": "[email protected]",
        "created_at": "2018-04-04 22:05:43",
        "updated_at": "2018-04-04 22:53:13",
        "api_token": "1QzaV8ebVUtrgF6qcvsmdL7S0Jh09tqR4tB1SBQ5tUacl0w2YWjzyLubXTgy"
    }
}

后面,可以拿着这个 api_token 作为令牌来请求需要认证的资源了。使用我们现有的策略,请求认证资源时,如果没有 token 或 token 错误,用户将会接收到未认证响应(401)。

创建退出接口

为了形成完整闭环,下面我们来编写退出登录接口,实现思路是用户发起退出登录请求时,我们将其对应的 api_token 字段值从数据库移除。

在 routes/api.php 中,添加路由:

Route::post('logout', 'Auth\LoginController@logout');

然后在 Auth\LoginController.php 中编写 logout 方法:

public function logout(Request $request)
{
    $user = Auth::guard('api')->user();

    if ($user) {
        $user->api_token = null;
        $user->save();
    }

    return response()->json(['data' => 'User logged out.'], 200);
}

使用该策略,一旦退出,用户的所有令牌都会失效,访问需要认证的 API 接口都会拒绝访问(通过中间件实现)。

这需要和前端配合来避免用户在没有访问任何内容的权限下保持登录状态。

请求头

通过 token 来保持用户的登录状态。因此,用户登录成功之后,请求后续的 API 接口时,需要传递下面的请求头信息。

 $headers = ['Authorization' => "Bearer $token"];

Auth::guard('api') 会根据这个请求头,来判断当前的用户信息,并判断用户的登录状态。

注:$token 的值就是 api_token 字段的值,用户每次登录成功后,api_token 都会刷新。

使用中间件限制访问

api_token 创建之后,我们就可以在路由文件中应用认证中间件了:

Route::middleware('auth:api')
    ->get('/user', function (Request $request) {
        return $request->user();
    });

我们可以使用 $request->user() 或 Auth 门面访问当前用户:

Auth::guard('api')->user(); // 登录用户实例
Auth::guard('api')->check(); // 用户是否登录
Auth::guard('api')->id(); // 登录用户ID    

接下来,我们将之前定义的文章相关路由进行分组:

Route::group(['middleware' => 'auth:api'], function() {
    Route::get('articles', 'ArticleController@index');
    Route::get('articles/{article}', 'ArticleController@show');
    Route::post('articles', 'ArticleController@store');
    Route::put('articles/{article}', 'ArticleController@update');
    Route::delete('articles/{article}', 'ArticleController@delete');
});

这样就不需要为每个路由单独设置中间件,好处是保持路由的DRY(Don’t Repeat Yourself)。

注:并不是所有的 API 接口都需要通过 auth 中间件认证过滤。

请求需要认证的资源时,如果请求头中没有 token 或 token 错误,就是认证失败,会抛出 AuthenticationException 异常。

AuthenticationException 异常是由 /Illuminate/Foundation/Exceptions/Handler.php 异常处理器的 unauthenticated() 方法来处理的。

protected function unauthenticated($request, AuthenticationException $exception)
{
    return $request->expectsJson()
                ? response()->json(['message' => $exception->getMessage()], 401)
                : redirect()->guest(route('login'));
}

如果想自定义认证失败后的响应,可以在 app/Exceptions/Handler.php 中重写该方法。

先添加

use Illuminate\Auth\AuthenticationException;

然后,重写 unauthenticated() 方法:

/**
 * Convert an authentication exception into a response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Auth\AuthenticationException  $exception
 * @return \Illuminate\Http\Response
 */
protected function unauthenticated($request, AuthenticationException $exception)
{
    return response()->json(['message' => $exception->getMessage()], 401);
}

这样的话,只要认证失败,就一定会返回自定义的 JSON 响应。

例如,直接访问 http://apidemo.test/api/articles/1 ,而没有传递带 token 的请求头信息的话,就会得到未认证响应(401)。

{
    "message": "Unauthenticated."
}

API 接口总结

现在,我们已经构建好了 RESTful 风格的 API 接口。

routes/api.php 文件中的 API 路由配置如下:

<?php

use Illuminate\Http\Request;

Route::post('register', 'Auth\RegisterController@register');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout');

Route::group(['middleware' => 'auth:api'], function() {
    Route::get('articles', 'ArticleController@index');
    Route::get('articles/{article}', 'ArticleController@show');
    Route::post('articles', 'ArticleController@store');
    Route::put('articles/{article}', 'ArticleController@update');
    Route::delete('articles/{article}', 'ArticleController@delete');
});

API 接口列表:

API 名称 请求方式 功能描述
/api/register POST 用户注册
/api/login POST 用户登录
/api/logout POST 退出登录
/api/articles GET 获取文章列表
/api/articles/{article} GET 获取指定的一篇文章
/api/articles POST 添加新文章
/api/articles/{article} PUT 更新文章
/api/articles/{article} DELETE 删除文章

测试接口

初始化设置

Laravel 开箱集成了 PHPUnit 进行测试,并且在项目根目录下为我们配置好了 phpunit.xml。

本教程中,我们使用内置的测试方法来测试上面编写的 API。

开始之前,我们需要做一些小调整以便使用内存级的 SQLite 数据库进行数据存储。

这样做的好处是可以让测试更快运行,但缺点是某些迁移命令可能不能正常运行,我的建议是当你遇到运行迁移命令出错或者更倾向于更加健壮的测试而不是高性能时不要使用 SQLite。

我们还会在每个测试之前运行迁移,这样就可以为每次测试构建数据库然后销毁掉,从而避免不同组测试间的相互干扰。

在 config/database.php 文件中,设置 sqlite 配置项中的 database 字段值为 :memory: 。

...
'connections' => [

    'sqlite' => [
        'driver' => 'sqlite',
        'database' => ':memory:',
        'prefix' => '',
    ],

    ...
]

然后在 phpunit.xml 中通过新增 DB_CONNECTION 环境变量来启用 SQLite:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="QUEUE_DRIVER" value="sync"/>
    <env name="DB_CONNECTION" value="sqlite"/>
</php>

基本配置已经完成,接下来就是配置 TestCase 在每次测试前运行迁移并填充数据库。为此,我们需要添加 DatabaseMigrations trait 然后在 setUp() 方法中添加 Artisan 调用。

修改 tests/TestCase.php 文件:

namespace Tests;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseMigrations;

    public function setUp()
    {
        parent::setUp();
        Artisan::call('db:seed');
    }
}

测试命令

运行 phpunit 测试命令:

# Windows 环境下需要用反斜线
vendor\bin\phpunit

# Linux 环境下用正斜线
vendor/bin/phpunit

如果报错信息为:

'vendor' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

请删除项目目录下的 vendor 目录,然后重新利用 composer 安装依赖。

composer install

如果报错信息为:

Error: Class 'Doctrine\DBAL\Driver\PDOSqlite\Driver' not found

这是因为没有安装 doctrine/dbal 扩展包,使用 Composer 安装即可:

composer require doctrine/dbal

测试命令的正常输出示例:

PHPUnit 6.5.7 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 522 ms, Memory: 14.00MB

OK (2 tests, 2 assertions)

创建模型工厂

模型工厂可以让我们快速生成测试数据,Laravel 开箱自带了 User 模型工厂,下面我们为 Article 类添加工厂:

php artisan make:factory ArticleFactory

模型工厂的存放目录是 database/factories。

编辑 ArticleFactory 类:

<?php

use Faker\Generator as Faker;
use App\Article;

$factory->define(Article::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence,
        'body' => $faker->paragraph,
    ];
});

编写测试用例

可以使用 Laravel 的断言方法对请求和响应进行测试。

下面我们来创建第一个测试用例 —— 登录测试

php artisan make:test LoginTest

编辑 tests/Feature/LoginTest.php 文件:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;

class LoginTest extends TestCase
{
    public function testRequiresEmailAndLogin()
    {
        $this->json('POST', 'api/login')
             ->assertStatus(422)
             ->assertJson([
                'message' => "The given data was invalid.",
                'errors'  => [
                    'email' => ['The email field is required.'],
                    'password' => ['The password field is required.']
                ]
             ]);
    }

    public function testUserLoginsSuccessfully()
    {
        $user = factory(User::class)->create([
            'email' => '[email protected]',
            'password' => bcrypt('test123'),
        ]);

        $payload = ['email' => '[email protected]', 'password' => 'test123'];

        $this->json('POST', 'api/login', $payload)
            ->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);
    }
}

注册测试

php artisan make:test RegisterTest

tests/Feature/RegisterTest.php 内容如下:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegisterTest extends TestCase
{
    public function testsRegistersSuccessfully()
    {
        $payload = [
            'name' => 'John',
            'email' => '[email protected]',
            'password' => 'toptal123',
            'password_confirmation' => 'toptal123',
        ];

        $this->json('post', '/api/register', $payload)
            ->assertStatus(201)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);;
    }

    public function testsRequiresPasswordEmailAndName()
    {
        $this->json('post', '/api/register')
             ->assertStatus(422);
    }

    public function testsRequirePasswordConfirmation()
    {
        $payload = [
            'name' => 'John',
            'email' => '[email protected]',
            'password' => 'toptal123',
        ];

        $this->json('post', '/api/register', $payload)
            ->assertStatus(422)
            ->assertJson([
                'message' => "The given data was invalid.",
                'errors'  => [
                    'password' => ['The password confirmation does not match.'],
                ]
            ]);
    }
}

退出测试

php artisan make:test LogoutTest

编辑 LogoutTest 代码如下:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;

class LogoutTest extends TestCase
{
    public function testUserIsLoggedOutProperly()
    {
        $user = factory(User::class)->create(['email' => '[email protected]']);
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        $this->json('get', '/api/articles', [], $headers)->assertStatus(200);
        $this->json('post', '/api/logout', [], $headers)->assertStatus(200);

        $user = User::find($user->id);

        $this->assertEquals(null, $user->api_token);
    }

    public function testUserWithNullToken()
    {
        // Simulating login
        $user = factory(User::class)->create(['email' => '[email protected]']);
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        // Simulating logout
        $user->api_token = null;
        $user->save();

        $this->json('get', '/api/articles', [], $headers)->assertStatus(401);
    }
}

注:在测试期间,Laravel 应用并不会在发起新请求时再次初始化,所以会在请求之间保存当前用户到 TokenGuard 实例,也因此我们不得不将退出测试一分为二,以避免受之前缓存用户的影响。

文章 API 接口测试:

php artisan make:test ArticleTest

编写 ArticleTest 代码如下:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;
use App\Article;

class ArticleTest extends TestCase
{
    public function testsArticlesAreCreatedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $payload = [
            'title' => 'Lorem',
            'body' => 'Ipsum',
        ];

        $this->json('POST', '/api/articles', $payload, $headers)
             ->assertStatus(201);
    }

    public function testsArticlesAreUpdatedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $article = factory(Article::class)->create([
            'title' => 'First Article',
            'body' => 'First Body',
        ]);

        $payload = [
            'title' => 'Lorem',
            'body' => 'Ipsum',
        ];

        $response = $this->json('PUT', '/api/articles/' . $article->id, $payload, $headers)
            ->assertStatus(200)
            ->assertJson([
                'title' => 'Lorem',
                'body' => 'Ipsum'
            ]);
    }

    public function testsArtilcesAreDeletedCorrectly()
    {
        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];
        $article = factory(Article::class)->create([
            'title' => 'First Article',
            'body' => 'First Body',
        ]);

        $this->json('DELETE', '/api/articles/' . $article->id, [], $headers)
             ->assertStatus(204);
    }

    public function testArticlesAreListedCorrectly()
    {
        factory(Article::class)->create([
            'title' => 'First Article',
            'body' => 'First Body'
        ]);

        factory(Article::class)->create([
            'title' => 'Second Article',
            'body' => 'Second Body'
        ]);

        $user = factory(User::class)->create();
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        $response = $this->json('GET', '/api/articles', [], $headers)
                         ->assertStatus(200)
                         ->assertJsonStructure([
                            '*' => ['id', 'body', 'title', 'created_at', 'updated_at'],
                         ]);
    }
}

最后,运行测试命令:

vendor\bin\phpunit

就可以对我们编写的所有测试用例进行测试,当然也会对 Laravel 默认提供的两个测试用例 ExampleTest 进行测试。

至此,我们已经完成了 API 接口的编写和测试,下一篇我们会基于 JWT 对 API 进行认证,同时整合 Vue SPA 做一个更偏向实战的教程。

猜你喜欢

转载自blog.csdn.net/lamp_yang_3533/article/details/84723996
今日推荐