PHP
Laravel
testing

Laravel で テスト〜API w/ JWT編


この記事について

Laravel で JWT を使って認証が必要な API のテストをする際の基本的な事柄について記載します。


はじめに


環境


  • PHP 7.2.2

  • Laravel 5.5.44

  • JWT(tymon/jwt-auth) 1.0.0-rc.3

  • PHPUnit 6.5.8

JWT ライブラリのバージョン以外は、この通りでなくても動作すると思います。


つくるもの

認証付きで、指定されたユーザーにアサインされた タスクリストを取ってくるエンドポイントを、テストファーストでつくります。

endpoint: GET /api/tasks

Parameters: assignee=nunulk


テストするもの


  • 上記エンドポイントを呼び出し、指定されたユーザー以外にアサインされたタスクアイテムが含まれないこと


準備


インストール

Laravel は割愛します。

jwt-auth 1.0 はまだ RC 版なので、バージョン指定する必要があります。

$ composer require "tymon/jwt-auth":"1.0.0-rc.3"


テスト用DBの設定

以下の記事を参考にしてください。

Laravel でテスト〜準備編 - Qiita

テスト用DBにマイグレーション流すのをお忘れなく。

$ php artisan migrate --database=mysql_testing


実装


テスト Step1

$ php artisan make:test Http/Controllers/TaskControllerTest

<?php

namespace Tests\Feature\Http\Controllers;

use Tests\TestCase;

class TaskControllerTest extends TestCase
{
/**
* assignee パラメータのテスト
*
* @return void
*/

public function testIndexWithAssignee()
{
$params = ['assignee' => 'nunulk'];
$response = $this->json('GET', '/api/tasks', $params);

$response->assertStatus(200);
}
}

ひとまずレスポンスのテストのみ。

実行

$ ./vendor/bin/phpunit tests/Feature/Http/Controllers/TaskControllerTest.php

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

F 1 / 1 (100%)

Time: 116 ms, Memory: 12.00MB

There was 1 failure:

1) Tests\Feature\Http\Controllers\TaskControllerTest::testIndexWithAssignee
Expected status code 200 but received 404.
Failed asserting that false is true.

当然 Fail


プロダクション Step1

$ php artisan make:controller Api/TaskController


routes/api.php

Route::group(['middleware' => 'auth:api'], function () {

Route::get('/tasks', 'TaskController@index');
});


TaskController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
public function index(Request $request)
{
return [];
}
}


再びテスト実行

$ ./vendor/bin/phpunit tests/Feature/Http/Controllers/TaskControllerTest.php

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

F 1 / 1 (100%)

Time: 240 ms, Memory: 14.00MB

There was 1 failure:

1) Tests\Feature\Http\Controllers\TaskControllerTest::testIndexWithAssignee
Expected status code 200 but received 401.
Failed asserting that false is true.

401 は認証エラー(Unauthorized)ですね、認証用トークンを渡してないので、当然の結果です。


テスト Step2

認証には実際の users レコードが必要なので、ファクトリを使って User を生成します。


TaskControllerTest.php

class TaskControllerTest extends TestCase

{
use DatabaseTransactions;

public function testIndexWithAssignee()
{
$userName = 'nunulk';
$user = factory(User::class)->create(['name' => $userName]);
$headers = ['Authorization' => 'Bearer ' . JWTAuth::fromUser($user)];
$params = ['assignee' => $userName];

$response = $this->json('GET', '/api/tasks', $params, $headers);

$response->assertStatus(200);
}
}


テスト実行

$ ./vendor/bin/phpunit tests/Feature/Http/Controllers/TaskControllerTest.php

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 505 ms, Memory: 18.00MB

OK (1 test, 1 assertion)

通りました。


テスト Step3

次に、「指定されたユーザー以外にアサインされたタスクアイテムが含まれないこと」を確認するために、タスクを追加していきます。

件名のみ Faker で作成するように、ModelFactory をつくっておきます。


database/factories/TaskFactory.php

<?php

use Faker\Generator as Faker;

$factory->define(App\Task::class, function (Faker $faker) {
return [
'subject' => $faker->text(),
];
});


テストコードの変更部分は下記の通りです。


TaskControllerTest.php

<?php

// (snip)
public function testIndexWithAssignee()
{
$userName = 'nunulk';
$user = factory(User::class)->create(['name' => $userName]);
factory(Task::class, 2)->create([
'created_by' => $user->id,
'assigned_to' => $user->id,
]);
factory(Task::class)->create([
'created_by' => $user->id,
'assigned_to' => null,
]);
$another = factory(User::class)->create();
factory(Task::class)->create([
'created_by' => $user->id,
'assigned_to' => $another->id,
]);
// (snip)

当該ユーザーにアサインされたタスクを2件、だれにもアサインされてないタスクを1件、別のユーザーにアサインされたタスクを1件つくります。

続いて、アサーションの部分に、返却された JSON に 'assignee.name' = 'nunulk' でないデータが含まれていないことを確認する処理を書きます。


TaskControllerTest.php

        $tasks = collect($response->decodeResponseJson());

$this->assertSame(2, $tasks->count());
$this->assertTrue($tasks->every(function (array $task) use ($userName) {
return $task['assignee']['name'] === $userName;
}));


every を使って全要素が条件を満たすのを確認したいので、JSON を配列に変換したあと Collection インスタンスに変換します。

念のため、要素数は 2 件であることも検証しておきます。

これでいったんテストを実行してみます。

$ ./vendor/bin/phpunit tests/Feature/Http/Controllers/TaskControllerTest.php

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

F 1 / 1 (100%)

Time: 597 ms, Memory: 18.00MB

There was 1 failure:

1) Tests\Feature\Http\Controllers\TaskControllerTest::testIndexWithAssignee
Failed asserting that 0 is identical to 2.

いまは空の配列を返しているので、要素数が 0 となり、テストは失敗します。

では、プロダクションコードの実装に入ります。


プロダクション Step3

ひとまず、assignee パラメータに対するバリデーションは行いません。

また、パラメータがない場合の処理も書きません。


TaskController.php

<?php

public function index(Request $request)
{
return Task::assignedTo($request->json('assignee'))->get();
}

スコープクエリメソッドをつくって、コントローラーはこんなかんじにスッキリとさせたいところです。

モデルはこんなかんじ。


Task.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
protected $with = [
'assignee',
'creator',
];

public function creator()
{
return $this->belongsTo(User::class, 'created_by', 'id');
}

public function assignee()
{
return $this->belongsTo(User::class, 'assigned_to', 'id');
}

public function scopeAssignedTo(Builder $query, string $name)
{
return $query->whereHas('assignee', function($query) use ($name) {
$query->where('name', $name);
});
}
}


いま関心があるのは assignee だけですが、ついでなので creator も書いちゃいました。

では、テストを実行してみます。

$ ./vendor/bin/phpunit tests/Feature/Http/Controllers/TaskControllerTest.php

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 590 ms, Memory: 18.00MB

OK (1 test, 3 assertions)

通りました。


おわりに

JWT 絡みのネタは JWTAuth::fromUser() のみになってしまった上、後半は API のテストとは関係ないかんじになってしまいましたが、ご容赦ください。

もし、こんな書き方もできるよ、などのアイデアがあれば、コメント欄にて教えていただけると助かります :bow: