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


原文请访问Laravel API Tutorial: How to Build and Test a RESTful API

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

Laravel是一个PHP框架,并注重开发人员的生产力。 由Taylor Otwell编写和维护,框架非常有意义,并努力通过有利于配置的惯例来节省开发人员时间。 该框架还旨在与Web一起发展,并已经在Web开发世界中纳入了几个新功能和想法,例如作业队列,开箱即用的API认证,实时通信等等。

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

RESTful API

首先,我们需要了解什么是RESTful API。 REST代表RE表示状态转移 ,是应用程序之间的网络通信的架构风格,它依赖于无状态协议(通常为HTTP)进行交互。

HTTP动词表示动作

在RESTful API中,我们使用HTTP动词作为动作,端点是被动作的资源。 我们将使用HTTP动词的语义:

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

更新操作:PUT与POST

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

资源

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

  • /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

安装完成后,您可以支持一个新的应用程序:

$ laravel new myapp

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

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

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

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

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

迁移和模型

在实际编写第一次迁移之前,请确保已为此应用程序创建了一个数据库,并将其凭据添加到位于项目根目录下的.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文档

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

$ php artisan make:model Article -m

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

<?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()方法;
  • $table->increments('id')设置名称为id的自动递增整数;
  • $table->timestamps()将为us- createdat和updatedat设置时间戳,但不要担心设置默认值,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 。 这样做,让我们继续迁移:

$ php artisan migrate

您也可以在 这里 使用 --step 选项,并将每个迁移分成自己的批处理,以便您可以根据需要单独回滚。

Laravel开箱即用,有两个迁移, createuserstable和createpasswordresetstable 。 我们不会使用passwordresets表,但是为我们准备好users表将是有帮助的。

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

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

可以使用Eloquent的 create() 和 update() 方法 对 $fillable 属性中的 字段 进行质量分配 。 您还可以使用 $guarded 属性来允许除属性外的所有属性。

数据库播种

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

$ php artisan make:seeder ArticlesTableSeeder

播种机将位于/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

让我们重复一下创建一个用户播放器的过程:

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

我们可以通过将种子添加到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限制中间件将自动应用于这些路由(如果要删除前缀,可以在RouteServiceProvider上编辑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行为。
  • 204 :没有内容。 当一个动作成功执行,但没有任何内容可以返回。
  • 206 :部分内容。 当您必须返回分页的资源列表时很有用。
  • 400 :请求不正确 无法通过验证的请求的标准选项。
  • 401 :未经授权 用户需要进行身份验证。
  • 403 :禁止 用户已通过身份验证,但没有执行操作的权限。
  • 404 :找不到 当没有找到资源时,这将由Laravel自动返回。
  • 500 :内部服务器错误。 理想情况下,您不会明确地返回此消息,但是如果发生意外中断,这是您的用户将会收到的。
  • 503 :服务不可用 相当自我解释,还有一个不会被应用程序显式返回的代码。

发送正确的404响应

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

我们可以通过编辑我们的异常处理程序类(位于app/Exceptions/Handler.php )来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标头,否则常规请求中的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请求将需要标题Accept: application/json 。

认证

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

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

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

然后实现迁移:

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

之后,只需运行迁移:

$ php artisan migrate

创建注册端点

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

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

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

在上面的部分中,我们使用用户模型中的一个方法来生成令牌。 这是有用的,所以我们只有一种方法来生成令牌。 在您的用户模型中添加以下方法:

class User extends Authenticatable  
{
    ...
    public function generateToken()
    {
        $this->api_token = str_random(60);
        $this->save();

        return $this->api_token;
    }
}

就是这样 用户现在已注册,并且由于Laravel的验证和开箱验证,需要使用name , email , password和password_confirmation字段,并自动处理反馈。 在RegisterController中查看validator()方法,看看规则是如何实现的。

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

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

现在,假设播种机已经运行,这是我们发送一个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"
    }
}

要在请求中发送令牌,您可以通过在有效负载中发送属性api_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  

我们得到如下结果:

这是因为我们需要在我们的Handler类上编辑当前unauthenticated方法。 当前版本只有在请求具有Accept: application/json标头的情况下才返回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');
});

这样我们就不必为每个路由设置中间件。 它现在不节省很多时间,但随着项目的增长,它有助于保持路由干扰。

测试我们的端点

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

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

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

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

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

...
'connections' => [

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

    ...
]

然后通过添加环境变量DB_CONNECTION在phpunit.xml启用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类,以便在每次测试之前使用迁移并种子数据库。 为此,我们需要添加DatabaseMigrations trait,然后在我们的setUp()方法中添加一个Artisan调用。 这是修改后的课程:

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类的工厂出来,所以我们为Article类添加一个:

$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 实例中,以避免再次 TokenGuard 数据库。 然而,一个明智的选择 - 在这种情况下,这意味着我们必须将注销测试分成两部分,以避免以前缓存的用户出现任何问题。

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

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设计黄金规则” 。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州