Web笔记 ·

Laravel API教程:如何构建和测试RESTful API

前言

本文原文:Laravel API Tutorial: How to Build and Test a RESTful API

这次一次来了两个没接触过的内容,一个与php的Laravel 有关,一个与Docker有关,由于Docker需要安装虚拟机什么的,就先以这个与Laravel有关的开篇。虽然会一步步跟着做,但由于php还只停留在几年前的初学阶段,以及个人英语水平所限,有些新名词可能会理解有误,翻译过程中难免出现错误之处,还请各位能见谅与指出或有能力也可以直接点击上面的链接查看英文原文。

文章正文

随着移动开发与JavaScript框架的兴起,使用RESTful API为数据与客户端之间构建单一接口成为最佳选择。

Laravel 是一个 专注提高开发人员生产力的php开发框架。 由Taylor Otwell撰写和维护,框架非常有意义,并努力通过支持 惯例优于配置原则( convention over configuration)来节省开发人员时间。该框架还旨在与Web一起发展,并已在Web开发界中纳入了几个新功能和想法,例如作业队列,开箱即用的API认证,实时通信等等。

Laravel API教程:如何构建和测试RESTful API

在本文中,我们将探讨如何构建和测试使用Laravel进行身份验证的强大API。我们将使用Laravel 5.4,所有的代码都可以在GitHub上参考。

RESTful APIs

首先,我们需要了解什么是RESTful API。 REST是 REpresentational State Transfer的简称, 是一种应用程序之间的网络通信的设计风格,它依赖无状态协议(通常为HTTP)进行交互。

HTTP动词表示动作(HTTP Verbs Represent Actions)

在RESTful API中,我们使用HTTP动词作为动作( actions),并且端点是所执行的资源。我们将使用HTTP动词的语义:

  • GET:检索资源
  • POST:创建资源
  • PUT:更新资源
  • DELETE:删除资源

Laravel API教程:如何构建和测试RESTful API

更新动作:PUT vs POST

RESTful API中有很多争论的问题,对于使用POSTPATCH或者PUT更新哪个是最好的,或者创建动作最好留给PUT动词这种问题有很多的意见。在本文中,我们将使用PUT更新操作,根据HTTP RFC,,PUT意味着在特定位置创建/更新资源。PUT动词的另一个要求是幂等,在这种情况下,基本上意味着您可以发送该请求1,2或1000次,结果将相同:数据库中的一个更新的资源。

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。

来源百度百科

资源(Resources)

资源将是actions的目标,在我们的文章和用户的情况下,他们有自己的端点:

  • /articles
  • /users

在这个laravel api教程中,资源将在我们的数据模型中具有1:1的表示,但这不是一个必须的要求。您可以将资源表示在多个数据模型中(或根本不在数据库中表示),并且模型完全不受用户限制。最后,您将以适合您的应用程序的方式来决定如何构建资源和模型。

关于一致性的说明

使用一组约定(如REST)的最大优点是您的API将更容易消费和开发。有些端点是非常明确,而且,作为一个结果,你的API将更加易于使用和维护,而不是这样的端点例如GET /get_article?id_article=12POST /delete_article?number=40。我在过去已经建立了糟糕的API,我仍然因为这个恨自己。

但是,将会很难映射到创建/检索/更新/删除模式。请记住,URL不应包含动词,资源不一定是表中的行。要记住的另一件事是,您不必为每个资源实施每个操作。

创建你的项目

与所有现代PHP框架一样,我们需要Composer来安装和处理我们的依赖关系。在您遵循下载说明(并添加到您的路径环境变量)后,使用以下命令安装Laravel:

$ composer global require laravel/installer

安装完成后,您可以像这样创建(手脚架 scaffold)一个新应用程序:

$ laravel new myapp

对于上面的命令,你需要确保~/composer/vendor/bin在你的$PATH。如果您不想处理,还可以使用Composer创建一个新项目:

$ composer create-project --prefer-dist laravel/laravel myapp

安装Laravel后,您应该可以启动服务器并测试一切正常工作:

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

Laravel API教程:如何构建和测试RESTful API

当您localhost:8000在浏览器上打开时,应该会看到这个示例页面。

迁移和模型(Migrations and Models)

在实际编写第一次迁移之前,请确保为此应用程序创建了一个数据库,并将其凭据添加到.env位于项目根目录中的文件中。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

您也可以使用专为Laravel特制的Vagrant盒子Homestead,但这远远超出了本文的范围。如果您想了解更多信息,请参阅Homestead文档

让我们开始我们的第一个模型和Migrations - 文章。该文章应该有一个标题和一个正文字段,以及创建日期。Laravel通过Artisan-Laravel的命令行工具提供了几个命令,可以通过生成文件并将其放在正确的文件夹中来帮助我们。要创建文章模型,我们可以运行:

$ php artisan make:model Article -m

-m选项是缩写--migration,它告诉Artisan为我们的模型创建一个。以下是生成的Migrations:

<?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->timestamps();
        });
    }

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

让我们解析一下:

  • up()down()将在我们分别Migration和回滚(rollback )时运行。
  • $table->increments('id')设置id为自动递增整数。
  • $table->timestamps() 将会为我们生成时间戳——在created_atupdated_at时,但是不用担心设置一个默认的,Laravel将在需要时更新这些字段。
  • 最后,Schema::dropIfExists()当然会丢弃表,如果存在的话。

有了这个,我们来添加两行到我们的up()方法:

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

string()方法创建VARCHAR等效列,同时text()创建等效列TEXT。这样做,让我们继续迁移(migrate):

$ php artisan migrate

您还可以使用 此处--step 选项,并将每个迁移(migration )分成自己的批处理,以便您可以在需要时单独回滚。

Laravel开箱即用自带的两个migrations,create_users_tablecreate_password_resets_table。我们不会使用password_resets表,但为我们准备好的users表将是有帮助的。

现在让我们回到我们的模型,并将这些属性添加到$fillable字段中,以便我们可以在我们Article::createArticle::update模型中使用它们:

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

$fillable 属性中的 字段可以使用Eloquent create() update() 方法 进行大量分配您也可以使用该$guarded 属性,以允许除属性外的所有属性。

Database Seeding

Database Seeding是使用我们可以用来测试数据库的虚拟数据填充我们的数据库的过程。Laravel带有Faker,一个伟大的为我们生成正确的虚拟数据格式的库。所以让我们创建我们的第一个seeder:

$ php artisan make:seeder ArticlesTableSeeder

Seeders将位于/database/seeds目录中。以下是我们设置创建几篇文章后的样子:

class ArticlesTableSeeder extends Seeder
{
    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,
            ]);
        }
    }
}

所以我们来运行seed命令:

$ php artisan db:seed --class=ArticlesTableSeeder

让我们重复一下之前的过程来创建一个用户seeder:

class UsersTableSeeder extends Seeder
{
    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' => 'admin@test.com',
            '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,
            ]);
        }
    }
}

我们可以通过将我们的seeders添加到database/seeds文件夹中的主DatabaseSeeder类中使其变得更容易:

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(ArticlesTableSeeder::class);
        $this->call(UsersTableSeeder::class);
    }
}

这样,我们可以简单地运行$ php artisan db:seed,它将在run()方法中运行所有被调用的类。

路由和控制器

我们为我们的应用程序创建基本端点:创建,检索列表,检索单个,更新和删除。在routes/api.php文件中,我们可以这样做:

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

内部的路由api.php/api/作为前缀,API限制中间件将自动应用于这些路由(如果你想要删除这个前缀可以编辑/app/Providers/RouteServiceProvider.php中的RouteServiceProvider)。

现在我们把这个代码移到自己的Controller上:

$ php artisan make:controller ArticleController

ArticleController.php:

use App\Article;
 
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 文件中:

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

我们可以通过使用隐式路由模型绑定来改进端点。这样,Laravel将Article在我们的方法中注入实例,如果没有找到,将自动返回404。我们必须对路由文件和控制器进行更改:

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

关于HTTP状态代码和响应格式的说明

我们还将response()->json()呼叫添加到我们的端点。这样我们可以明确地返回JSON数据以及发送客户端可以解析的HTTP代码。你将要返回的最常见的代码是:

  • 200: 好。标准成功代码和默认选项。
  • 201:创建对象。有用的store行动(action)。
  • 204: 无内容。当一个动作执行成功,但没有内容返回。
  • 206:部分内容。当您必须返回分页的资源列表时很有用。
  • 400: 错误的请求。无法通过验证的请求的标准选项。
  • 401:未经授权 用户需要进行身份验证。
  • 403:禁止 用户已通过身份验证,但没有执行操作的权限。
  • 404: 未找到。当没有找到资源时,这将由Laravel自动返回。
  • 500: 内部服务器错误。理想情况下,你不会明确地返回这个,但如果有意外的中断,这是你的用户将要收到的。
  • 503: 暂停服务。相当自我解释,还有一个不会被应用程序显式返回的代码。

发送正确的404响应

如果您尝试获取不存在的资源,则会抛出异常,您将收到整个堆栈跟踪,如下所示:

Laravel API教程:如何构建和测试RESTful API

我们可以通过编辑在app/Exceptions/Handler.php中我们的异常处理程序类来修复它,以返回JSON响应:

public function render($request, Exception $exception)
{
    // This will replace our 404 response with
    // a JSON response.
    if ($exception instanceof ModelNotFoundException) {
        return response()->json([
            'error' => 'Resource not found'
        ], 404);
    }

    return parent::render($request, $exception);
}

这是一个返回的例子:

{
    data: "Resource not found"
}

如果您使用Laravel服务其他页面,则必须编辑代码以使用Accept header,否则常规请求中的404错误也将返回JSON。

public function render($request, Exception $exception)
{
    // This will replace our 404 response with
    // a JSON response.
    if ($exception instanceof ModelNotFoundException &&
        $request->wantsJson())
    {
        return response()->json([
            'data' => 'Resource not found'
        ], 404);
    }

    return parent::render($request, $exception);
}

在这种情况下,API请求将需要header为 Accept: application/json

认证

在Laravel中有许多实现API身份验证的方法(其中之一是Passport,实现OAuth2的好方法),但在本文中,我们将采用一个非常简化的方法。

开始使用前,我们需要在users表中添加一个api_token字段:

$ php artisan make:migration --table=users adds_api_token_to_users_table

然后实现migration:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('api_token', 60)->unique()->nullable();
    });
}

public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn(['api_token']);
    });
}

创建注册端点

我们将使用RegisterController(在Auth文件夹中)在注册时返回正确的响应。Laravel随身携带身份验证,但我们仍然需要调整一下以返回我们想要的答复。

控制器利用RegistersUsers trait来实现注册。这是它的工作原理:

public function register(Request $request)
{
    // Here the request is validated. The validator method is located
    // inside the RegisterController, and makes sure the name, email
    // password and password_confirmation fields are required.
    $this->validator($request->all())->validate();

    // A Registered event is created and will trigger any relevant
    // observers, such as sending a confirmation email or any 
    // code that needs to be run as soon as the user is created.
    event(new Registered($user = $this->create($request->all())));

    // After the user is created, he's logged in.
    $this->guard()->login($user);

    // And finally this is the hook that we want. If there is no
    // registered() method or it returns null, redirect him to
    // some other URL. In our case, we just need to implement
    // that method to return the correct response.
    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}

我们只需要在我们的 RegisterController.中实现 registered() 方法。该方法接收$request$user,所以这真的是我们想要的。这是控制器内部的方法应该如下:

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

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

我们可以链接到路由文件:

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

就是这样,用户现在注册并由于Laravel的验证和开箱验证,在nameemailpassword,和password_confirmation为必填字段,并且反馈自动处理。检测RegisterController中的validator()方法,看看规则是如何实现的。

当我们点击该端点(endpoint)时,我们得到的是:

$ curl -X POST http://localhost:8000/api/register \
 -H "Accept: application/json" \
 -H "Content-Type: application/json" \
 -d '{"name": "John", "email": "john.doe@toptal.com", "password": "toptal123", "password_confirmation": "toptal123"}'
{
    "data": {
        "api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT",
        "created_at": "2017-06-20 21:17:15",
        "email": "john.doe@toptal.com",
        "id": 51,
        "name": "John",
        "updated_at": "2017-06-20 21:17:15"
    }
}

创建登录端点

就像注册端点一样,我们可以编辑LoginController(在Auth文件夹中)来支持我们的API认证。 该 AuthenticatesUsers trait的login 方法可以被覆盖以支持我们的API:

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

我们可以链接到路由文件:

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

现在,假设seeders 已经运行,当我们POST向该路由发送请求时,我们得到的是:

$ curl -X POST localhost:8000/api/login \
  -H "Accept: application/json" \
  -H "Content-type: application/json" \
  -d "{\"email\": \"admin@test.com\", \"password\": \"toptal\" }"
{
    "data": {
        "id":1,
        "name":"Administrator",
        "email":"admin@test.com",
        "created_at":"2017-04-25 01:05:34",
        "updated_at":"2017-04-25 02:50:40",
        "api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw"
    }
}

在请求中发送令牌token,您可以通过api_token在有效负载中发送属性或以请求头中的承载token形式(格式如下)来执行此操做:Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw

注销

使用我们当前的策略,如果令牌错误或丢失,用户应该收到未经身份验证的响应(我们将在下一节中实现)。因此,对于一个简单的注销端点,我们将发送令牌,它将在数据库上删除。

routes/api.php:

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

Auth\LoginController.php:

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将拒绝访问(使用中间件,如下一节所述)。这需要与前端进行协调,以避免用户在没有访问任何内容的情况下保持记录。

使用中间件限制访问

通过api_token创建,我们可以切换路由文件中的身份验证中间件:

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

我们可以使用该$request->user()方法或通过Auth Facade 访问当前用户

Auth::guard('api')->user(); // instance of the logged user
Auth::guard('api')->check(); // if a user is authenticated
Auth::guard('api')->id(); // the id of the authenticated user

我们得到如下结果:

Laravel API教程:如何构建和测试RESTful API

这是因为我们需要在我们的Handler类上编辑当前的 unauthenticated方法。当前的版本只有在请求具有Accept: application/json头(header)时才返回JSON ,所以我们来更改它:

protected function unauthenticated($request, AuthenticationException $exception)
{
    return response()->json(['error' => 'Unauthenticated'], 401);
}

有了这个修正,我们可以回到文章的终点来将它们包装在auth:api中间件中。我们可以通过使用路由组来做到这一点:

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。

测试我们的端点

Laravel包含通过phpunit.xml已经设置的PHPUnit开箱即用的集成。该框架还为我们提供了几个帮助者和额外的断言,使我们的生活更容易,特别是测试API。

您可以使用许多外部工具来测试您的API; 然而,Laravel内部的测试是一个更好的选择 - 我们可以拥有测试API结构和结果的所有好处,同时保留对数据库的完全控制。对于列表端点,例如,我们可以运行几个工厂,并声明响应包含这些资源。

要开始使用,我们需要调整一些设置来使用内存中的SQLite数据库。使用它将使我们的测试快速运行,但是权衡是一些迁移(migration )命令(例如约束)在该特定设置中将无法正常工作。我建议您在开始获取迁移错误时,在测试中离开SQLite,或者您希望使用更强大的测试,而不是执行运行。

我们还将在每次测试之前运行migrations 。此设置将允许我们为每个测试构建数据库,然后将其破坏,避免测试之间的任何类型的依赖关系。

在我们的config/database.php文件中,我们需要databasesqlite配置中的字段设置为:memory:

...
'connections' => [

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

然后在phpunit.xml通过添加环境变量 DB_CONNECTION启用S​​QLite:

  <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类,以便在每次测试之前使用迁移(migrations )并种子(seed )数据库。为此,我们需要添加DatabaseMigrations trait,然后添加一个Artisan  call在我们的setUp()方法上。这是修改后的class:

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

我最喜欢做的是将测试命令添加到composer.json

    "scripts": {
        "test" : [
            "vendor/bin/phpunit"
        ],
    ... 
    },

测试命令将如下所示:

$ composer test

为我们的测试设立工厂

工厂将允许我们快速创建具有正确数据进行测试的对象。它们位于database/factories文件夹中。Laravel自带一个User class 上的工厂,所以我们为Article class 添加一个:

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

Faker库已经注入,以帮助我们为我们的模型的随机数据的正确格式。

我们的第一个测试

我们可以使用Laravel的断言方法轻松击中一个端点并评估其响应。我们创建我们的第一个测试,登录测试,使用以下命令:

$ php artisan make:test Feature/LoginTest

这是我们的测试:

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


    public function testUserLoginsSuccessfully()
    {
        $user = factory(User::class)->create([
            'email' => 'testlogin@user.com',
            'password' => bcrypt('toptal123'),
        ]);

        $payload = ['email' => 'testlogin@user.com', 'password' => 'toptal123'];

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

    }
}

这些方法测试了几个简单的情况。该json()方法触发端点,而其他断言是非常自明的。一个细节assertJson():此方法将响应转换为数组搜索参数,因此顺序很重要。assertJson()在这种情况下,您可以链接多个呼叫。

现在,我们创建注册端点测试,并为该端点写一对:

$ php artisan make:test RegisterTest
class RegisterTest extends TestCase
{
    public function testsRegistersSuccessfully()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@toptal.com',
            '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)
            ->assertJson([
                'name' => ['The name field is required.'],
                'email' => ['The email field is required.'],
                'password' => ['The password field is required.'],
            ]);
    }

    public function testsRequirePasswordConfirmation()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@toptal.com',
            'password' => 'toptal123',
        ];

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

最后,注销端点:

$ php artisan make:test LogoutTest
class LogoutTest extends TestCase
{
    public function testUserIsLoggedOutProperly()
    {
        $user = factory(User::class)->create(['email' => 'user@test.com']);
        $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' => 'user@test.com']);
        $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 实例中,以避免再次触发数据库。然而,一个明智的选择 - 在这种情况下,这意味着我们必须将注销测试分为两个,以避免与先前缓存的用户有任何问题。

测试文章的终点(endpoints )也很简单:

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(200)
            ->assertJson(['id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum']);
    }

    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([ 
                'id' => 1, 
                '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)
            ->assertJson([
                [ 'title' => 'First Article', 'body' => 'First Body' ],
                [ 'title' => 'Second Article', 'body' => 'Second Body' ]
            ])
            ->assertJsonStructure([
                '*' => ['id', 'body', 'title', 'created_at', 'updated_at'],
            ]);
    }

}

下一步

这就是它的一切。绝对有改进的空间 - 您可以使用Passport软件包实现OAuth2 ,集成分页和转换层(我推荐使用Fractal),但是我想通过在Laravel中创建和测试API的基础知识外部包装。

Laravel肯定提高了我对PHP的经验,并且易于使用测试巩固了我对该框架的兴趣。这不完美,但它足够灵活,可以让您解决问题。

如果您正在设计一个公共API,请参阅“Great Web API设计黄金规则”

参与评论