Dev

【実務入門】Laravel8.xでメモ・ToDoアプリ作成/PHPUnitでのテストまでのチュートリアル

ぺん
ぺん
Laravel8出てから全く触ってなかったよな
この記事を書いた人
この記事を書いた人
そうそう。今回はLaravel8を使いつつ、メモアプリを作ってみるよ。ググって出てくるLaravelチュートリアルが少しずつ古くなってきたので改めてまとめておきます

この記事の想定読者
✔︎ PHPを調べながら読み書きできるかな?レベルの人
✔︎ 写経ではなく実務で使えるチュートリアルを探している人
✔︎ 現役フリーランスエンジニアの開発手順をなぞってみたい人
✔︎ LaravelでPHPUnitも入門しておきたい人

ぺん
ぺん
一応、M1(最新)ではないMacを準拠して解説していくよ。


この記事の開発環境
OS: macOS Catalina
PC: MacBook Pro (16-inch, 2019) : CPUがギリギリIntelのもの
ターミナル: zsh
Docker: Docker for Mac(Docker Engine v20.10.2)

Docker for Macが必要になります。。

ぺん
ぺん
WordPressの開発でもDockerの方が楽だぞ〜
この記事を書いた人
この記事を書いた人
とりあえず設定ファイルがあればコマンド一発で環境構築が終わるので、ぜひ入れてみてください😌

>> Docker のインストールはこちら

ソースコードについてはgithubにおいています。ぜひ参考やクローンいただければと思います。😌

>> コードはこちら(github)

スポンサードサーチ

ページコンテンツ

Laravel8.xプロジェクトを始める

この記事を書いた人
この記事を書いた人
さっそく、プロジェクトをセットアップしてみましょう
ぺん
ぺん
セットアップまわりで詰まったらLaravel公式を参照してくれい!

>> Laravel8.x系のドキュメント(英)
>> Laravel8.x系のドキュメント(日)

▲稀に翻訳待ちになるので、最新情報は英語版を参照してください。

1. Laravel8.x系のダウンロード(curl)

[simterm] curl -s https://laravel.build/memo-app | bash
[/simterm] 

コマンドラインでインストールしたいディレクトリで実行します。

途中でパスワード聞かれたら入力必要です。
インストール完了次第、dockerコンテナの起動コマンドが表示されます。

2. Dockerコンテナ起動

エンジニア
エンジニア
Docker for Macを起動しておいてください
[simterm]cd memo-app && ./vendor/bin/sail up[/simterm] [simterm] docker ps
[/simterm]

▲docker psをしてコンテナが見つかれば成功

3.Localhostへアクセスしてみる

ひとまずプロジェクト作成が完了!

メモアプリの設計と簡易アーキテクチャ

ぺん
ぺん
今回は可愛いオタクが推しのためのメモアプリを作成して欲しいらしいなw
この記事を書いた人
この記事を書いた人
要件があってこその開発なので、ひとまず要件に落として設計するところから考えてみましょう

実際の現場だとさらに複雑度が上がったりしますが、ひとまず雰囲気をお伝えできればとw

したいこと・実現したいこと

可愛いオタク女子はエンジニアの気持ちを汲み取ったのか、簡単なCRUD機能を備えたメモアプリを要求しました。

認証や公開、ユーザーログインといった面倒な物は不要で、個人的な推しの好きなところをメモしたいそうです。

オタク
オタク
あ、一応今回はWEBでいいけど、スマホアプリ化もしたいからバックエンドAPIで.jsonだけ投げ返してくれたらOKよ
ぺん
ぺん
(エンジニアに優しいオタクだ…)

エンジニアに最後まで寄り添った要件のようです。とりあえずTwitter風にメモできると良いそうなので、一度これで作ってみましょう。

DB設計

ぺん
ぺん
まあとりあえず最低限のメモ機能ならIDとメモ内容だけでOKだな
エンジニア
エンジニア
ひとまず要件を最低限満たすにはこの2つさえ永続化できれば良さそうですね。

アーキテクチャ

エンジニア
エンジニア
Laravelのプロジェクトで比較的よく見る設計でやってみましょう。

 

▲ひとまずEloquentモデルとEloquentORMの利用をModelとRepositoryに分離します

エンジニア
エンジニア
ひとまずフロントエンド部分は置いておいて、バックエンドのテストまでを作成して、そこからフロントエンド(View)との繋ぎ込みを試みます
ぺん
ぺん
バックエンドできたら今度フロントエンドはvue.jsで作ってみるぞ〜

Model
Eloquent モデルを配置する

Repository
Eloquent ORMクエリを作成する

Controller
今回はAPIなのでJSONを返すよう実装する。

スポンサードサーチ

モデル・Factory・マイグレーションの作成

Noteモデル作成

[simterm]php artisan make:model Note[/simterm]

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Note extends Model
{
    use HasFactory;
}

僕
特に変更はありません。このままでいきましょう。
ぺん
ぺん
Eloquentモデル定義がないとデータベースとやりとりできないからやむなしだな

>> コードはこちら(github)

Noteモデル用のFactory作成

[simterm]php artisan make:factory NoteFactory[/simterm]

<?php

namespace Database\Factories;

use App\Models\Note;
use Illuminate\Database\Eloquent\Factories\Factory;

class NoteFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Note::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'note_contents' => 'hogehogehogehoge'
        ];
    }
}

僕
note_contentsはメモ内容なので好きな文字列で適当に入れておいてください。

>> コードはこちら(github)

NotesDBマイグレーション作成

[simterm]php artisan make:migration create_notes_table[/simterm]

<?php

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

class CreateNotesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('notes', function (Blueprint $table) {
            $table->id();
            $table->string('note_contents', 255);
            $table->timestamps();
        });
    }

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

Twitterのように140字をnote_contentsへ保存する予定ですが、ひとまずvarchar255相当のstringとしておきます。

僕
ここまでできたらひとまずDBとの疎通確認とEloquentの準備ができているか確認してみましょう。

ここまでの疎通確認

ぺん
ぺん
基本はDockerコンテナのなかで作業するぞい

プロジェクトディレクトリ直下でコマンドを実行します。

[simterm]docker-compose exec laravel.test bash //dockerコンテナへ[/simterm] [simterm]php artisan migrate //DBマイグレート[/simterm] [simterm]php artisan tinker //REPL: tinker起動[/simterm]
Psy Shell v0.10.5 (PHP 8.0.0 — cli) by Justin Hileman
>>> use App\Models\Note;
>>> $note = Note::factory()->make(); //factory生成
=> App\Models\Note {#3350
    note_contents: "hogehogehogehoge", //モデルにサンプルレコード挿入
}
>>> $note->save(); //モデルからsave()保存
=> true
>>> Note::all(); //Noteレコードを全件取得
=> Illuminate\Database\Eloquent\Collection {#3658
    all: [
         App\Models\Note {#4026
         id: 1,
         note_contents: "hogehogehogehoge",
         created_at: "2021-01-13 11:16:04",
         updated_at: "2021-01-13 11:16:04",
        },
    ],
}

▲REPLでDB/Eloquent疎通確認をサクッと行う

ぺん
ぺん
ひとまずモデルでデータ作成して、factory経由でデータをモデルに埋め込んで、それをデータベースに保存はできるっぽいな
僕
ここまででもし詰まったらコメントしていただくか、セットアップの見直しをお願いします😂

モデルまでのUnitテストを書く

ぺん
ぺん
さっき、php artisan tinkerで行ったモデル~factory~DB保存までの一連の流れを改めてPHPUnitで自動テスト書いとくか
僕
うん、ひとまず実務でもこの辺りで書いとくのが進めやすいと思います。(自分は少なくともやりやすいです笑😌

ここまででまずはテストの疎通チェック、初めてのテストを書いてみます。

Laravel8ではphpunitに対して特に設定も行うことなく、コンテナを起動した時からphpunit連携ができます。

テスト用環境変数.env.testingを追加

.env.testsingをプロジェクトディレクトリへ追加。


APP_ENV=testing
APP_KEY=
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=test_database
DB_USERNAME=root
DB_PASSWORD=

>> githubでコードを見る

テスト用APP_KEYを生成

を生成します。

テスト用のAPP_KEYがない場合は.envAPP_KEYを参照してしまいます。実行環境のデータをテスト時にマイグレーション更新してしまう(データが飛ぶ)ので注意が必要です。

[simterm]php artisan key:generate –env=testing[/simterm]

PHPUnitの設定ファイルphpunit.xmlを変更


<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="mysql"/>
        <env name="DB_DATABASE" value="test_database"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

APP_ENVに先述のtestingを指定 / DB_DATABASEにtest_databaseを指定 

この記事を書いた人
この記事を書いた人
test_databaseがなくてエラーが出る場合はmysqlコンテナ内で新規でデータベース作成しておいてください

>> githubでコードを見る

PHPUnitを実際に実行

コンテナ内でphpunitを実行してみましょう。

[simterm]php artisan test //artisanから[/simterm] [simterm]./vendor/bin/phpunit //コンテナ内で[/simterm]

▲デフォルトでサンプルテストがあるので疎通していればテストが通ります。

Noteモデルに対するテストファイルを準備する

ここでモデルのテストの用意をします。
[simterm]php artisan make:test NoteTest –unit[/simterm]▲./test/Unit 直下にNoteTest.phpが生成される

改めて、./test/Unit/Modelsフォルダを作成し、./test/Unit/Models/NoteTest.phpという配置にしておきましょう。namespaceも修正必要です。

>> githubでコードを見る

僕
ひとまず簡単なモデル・factoryのチェック、DB疎通チェック、phpunitが動作するのかをテストしてみます。この時、./Tests/Feature/ExampleTest.phpは不要なので削除してください。

<?php

namespace Tests\Unit\Models;

use Tests\TestCase;
use App\Models\Note;
use Illuminate\Foundation\Testing\RefreshDatabase; //テストごとにDBリフレッシュ

class NoteTest extends TestCase
{
    use RefreshDatabase;

    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function test_example()
    {
        $this->assertTrue(true);
    }

    public function testModelAndFactory() 
    {        
        $persisted = Note::factory()->create(); //永続化(内部的にsaveを行う)
        $first = Note::first(); //最初のモデルを取得

        $this->assertEquals($persisted->id, $first->id);
    }
}

>> githubでコードを見る▲そもそもコマンドで作成したテストスケルトンファイルが正常に動作しないのでこちらで書き換えてください

testModelAndFactory()というメソッドでは、factoryで作成したモデルをcreate()メソッドにより、永続化(DBに保存をかける)それがNoteモデルで取得できる最初の1レコードと同じであることをアサートしています。

[simterm]php artisan test //artisanから[/simterm] [simterm]./vendor/bin/phpunit //コンテナ内で[/simterm]

こちらのテストが通ればOKです。

スポンサードサーチ

RepositoryでEloquentORMを書いていく

NoteRepositoryを作成


<?php

namespace App\Repositories;

use App\Models\Note;

class NoteRepository
{  
  /**
   * @var Note
   */
  private $note;
  
  /**
   * @param  Note $note
   */
  public function __construct(Note $note)
  {
    $this->model = $note;
  }
}

▲./App/Repositoriesフォルダを新規で作成。その中にNoteRepository.phpを作成。 >> githubでコードを見る

仕様としては、メモの作成(新規作成)メモの編集(アップデート)メモの一覧(リスト)メモの削除(削除)これら4つの動詞があるように見えます。

それぞれ、EloquentORMで下ろしてみると次のようになりそうです。


<?php

namespace App\Repositories;

use App\Models\Note;
use Illuminate\Database\Eloquent\Collection;


class NoteRepository
{  
  /**
   * @var Note
   */
  private $note;
  
  /**
   * @param  Note $note
   */
  public function __construct(Note $note)
  {
    $this->model = $note;
  }
  
  /**
   * メモの一覧取得
   *
   * @return Collection
   */
  public function list(): Collection
  {
    return $this->model->get();
  }
  
  /**
   * メモの保存/更新
   *
   * @param  array $params
   * @return Note
   */
  public function upsert(array $params): Note
  {
    return $this->model->updateOrCreate(
      ['id' => $params['id']],
      ['note_contents' => $params['note_contents']]
    );
  }
  
  /**
   * メモの削除
   *
   * @param  int $id
   * @return bool
   */
  public function destroy(int $id): bool
  {
    return $this->model->destroy($id);
  }
}

>> githubでコードを見る▲ひとまずCRUDを実装してみる

ぺん
ぺん
まあ、よくある実装だけどこれで動きそうか?

 

この記事を書いた人
この記事を書いた人
うん、ひとまずRepositoryでEloquentORM書いたら速攻Unitテスト書く方がいいね。ひとまず書いてみよう。

Repositoryに対するUnitテストを書きながら検証を重ねる

この記事を書いた人
この記事を書いた人
ひとまず書いてみた…けど、あ、やっぱりRepositoryのミス見つけた。

<?php

namespace Tests\Unit\Repositories;

use Tests\TestCase;
use App\Models\Note;
use App\Repositories\NoteRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;

class NoteRepositoryTest extends TestCase
{
  use RefreshDatabase;

  public function setUp(): void
  {
    parent::setUp();
    $this->repo = \App::make(NoteRepository::class); // IoC containerによる自動解決
  }

  public function testList()
  {
    $n = 5;
    $notes = Note::factory()->count($n)->create(); //レコード5件作成
    $list = $this->repo->list(); //NoteRepositoryの実装のメソッドlist()を実行

    $this->assertEquals($n, count($list)); //実際に5件のCollectionがリストされるか確認
  }

  public function testUpsert()
  {
    $params = [
      'id' => 10,
      'note_contents' => 'new hogefuga'
    ];

    $upsert = $this->repo->upsert($params);

    $this->assertTrue($upsert); //保存成功すればTrueを返却
  }
}

>> githubでコードを見る

▲./tests/Unit/Repositories/ディレクトリを新規作成。NoteRepositoryTest.phpを作成。 

ぺん
ぺん
モデルにmass assignment(Laravelの場合は大量のデータ作成・更新の際に、モデルの$fillableを確認して挿入OKかを確認しています)のための設定がなかったな。

 

この記事を書いた人
この記事を書いた人
OK〜。モデルに戻って追記してみよう。今回はメモ内容だけがmass assignment設定だね。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Note extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['note_contents'];
}

>> githubでコードを見る▲$fillable設定をNoteモデルに追加

ぺん
ぺん
よし、再度テストを回してみよう。コンテナ内で./vendor/bin/phpunitだな
この記事を書いた人
この記事を書いた人
OK、今回は通ったね。緑だ。

▲今回はテストが通る

こんな感じで、実務でも自動テストで担保しながら、検証を重ねてコードを書いていきます。


<?php 

namespace Tests\Unit\Repositories; 

use Tests\TestCase; 
use App\Models\Note; 
use App\Repositories\NoteRepository; 
use Illuminate\Foundation\Testing\RefreshDatabase; 

class NoteRepositoryTest extends TestCase 
{ 
  use RefreshDatabase; 

  public function setUp(): void 
  { 
    parent::setUp(); 
    $this->repo = \App::make(NoteRepository::class); // IoC containerによる自動解決
  }

  public function testList()
  {
    $n = 5;
    $notes = Note::factory()->count($n)->create(); //レコード5件作成
    $list = $this->repo->list(); //NoteRepositoryの実装のメソッドlist()を実行

    $this->assertEquals($n, count($list)); //実際に5件のCollectionがリストされるか確認
  }

  public function testUpsertCreate()
  {
    $params = [
      'id' => null,
      'note_contents' => 'new hogefuga' //新規作成するとき
    ];

    $upsert = $this->repo->upsert($params);

    $this->assertInstanceOf(Note::class, $upsert); //保存成功すればNoteを返却  
  }
  
  /**
   * Upsertのうち、Updateが正常に行えるか
   *
   * @return void
   */
  public function testUpdateOfUpsert()
  {
    // レコード新規作成
    $note =  Note::factory()->create([
      'id' => 100,
      'note_contents' => 'hogehogehogehoge'
    ]);

    // アップデートパラメーター
    $params = [
      'id' => 100,
      'note_contents' => 'fuga'
    ];

    // 更新: 成功すればNoteモデルを返却
    $upsert = $this->repo->upsert($params);
 
    $this->assertInstanceOf(Note::class, $upsert);
    // upsertして返却されたnote_contents = アップデートパラメータ note_contents
    $this->assertEquals(
      $params['note_contents'], 
      $upsert->toArray()['note_contents']
    ); 
  }
  
  /**
   * Upsertのうち、Createが正常に行えるか
   *
   * @return void
   */
  public function testCreateOfUpsert()
  {
    // 新規作成レコード
    $params = [
      'id' => null,
      'note_contents' => 'hogehogehogehoge'
    ];

    // 新規作成: 成功すればNoteモデルを返却
    $upsert = $this->repo->upsert($params);

    // 保存成功すればNoteを返却  
    $this->assertInstanceOf(Note::class, $upsert);
  }

  public function testDestroy()
  {
    $id = 200;

    // レコード新規作成
    $note =  Note::factory()->create([
      'id' => $id,
      'note_contents' => 'hogehogehogehoge'
    ]);

    // モデル destroy
    $destroy = $this->repo->destroy($id);

    // 成功すれば真偽値でtrueを返す
    $this->assertTrue($destroy);

  }
}

>> githubでコードを見る

▲ひとまず緑になりました

ぺん
ぺん
なるほど。とりあえずRepositoryのテストとしてはこんなもんでいいんじゃないか? あ、そういえばテストでガチャガチャデバッグしてたりしたけど、それのやり方も一応書いといてくれ。
この記事を書いた人
この記事を書いた人
記事で書いているので速攻コードが生産されたように見えてますが、結構ガチャガチャとテスト中にデバッグしたりしていますw😅

  /**
   * Upsertのうち、Updateが正常に行えるか
   *
   * @return void
   */
  public function testUpdateOfUpsert()
  {
    // レコード新規作成
    $note =  Note::factory()->create([
      'id' => 100,
      'note_contents' => 'hogehogehogehoge'
    ]);

    // アップデートパラメーター
    $params = [
      'id' => 100,
      'note_contents' => 'fuga'
    ];

    // 更新: 新規作成したモデルを配列にシリアライズ
    $upsert = $this->repo->upsert($params);

    dd($upsert->toArray()); //🤔 モデルから配列に変換できたっけ?

    // 保存成功すればNoteモデルを返却  
    $this->assertInstanceOf(Note::class, $upsert);
    // upsertして返却されたnote_contents = アップデートパラメータ note_contents
    $this->assertEquals(
      $params['note_contents'], 
      $upsert->toArray()['note_contents']
    ); 
  }

▲$upsert->toArray()の変数が出る

この記事を書いた人
この記事を書いた人
dd()してデバッグしたりします。テスト最中にdd()すると、このコマンドは処理を止める→変数をキレイにして表示 という処理を行うのですが、その際にテストデータがおかしな挙動をする可能性があります…
ぺん
ぺん
まあ推奨ではないけど、print_r()とか整形されないデバッグ結果見ても生産性なさすぎるから、開発環境でやるならとりあえずいいんじゃないか😔

ひとまず、Repository層でのCRUD実装はこれで完了です。

ControllerでJsonレスポンスを返却する

【テスト必須】コントローラーの実装は必ずテスタブルに!

ぺん
ぺん
テスタブルなControllerを作るコツその1! コントローラーの処理は適当にして返却すべきものだけに集中!
この記事を書いた人
この記事を書いた人
もう基本的に大規模で複雑なアプリでもない限りは、なるべくControllerをうすーーーくしましょう。そのためには開発段階から必要最低限のものだけ載せていきます。

ペンギンがいいことを言っています。コントローラの処理は適当であるべきです。

適当とは、その処理自体が適切に交換可能であったり、テスタブルであることです。

コントローラにはありとあらゆる責務(やるべきこと)が乗ってしまいます。小規模の時は気になりませんが、大規模になればなるほどコントローラの処理が全く読めない意味不明なものになっていきます。

意識して処理を適切に責務を移譲したとしても、コントローラは正直全く意味不明になりがちです。なので、絶対ではないですが「テスタブルな実装をしていく」というのが必要不可欠です。そのため、コントローラーのテストは絶対書きましょう。

ぺん
ぺん
逆にコントローラのテストを書くためにRepositoryやModelのテストをやってるようなもんだな😩😩 慣れてきたら作業ゲーすぎるぞこれ
この記事を書いた人
この記事を書いた人
Model~RepositoryでEloquentの処理を分けると、この辺の作成は正直作業ですね😅 (やることはあんまり変わらない…w)

とりあえず、テストコードを書きましょう。テストコードを書くことで、圧倒的に実力が早く伸びます(これは保証します。)

  1. テストコードを書くこと = フレームワークを実装レベルで理解していないとできない
  2. テストコードを書くことで、フレームワークのことを嫌でも実装レベルで調べざるを得なくなる
  3. テストコードを書くことで、テスタブルなコードを書くことが、綺麗な実装を生み出すことを理解できる
  4. テストコードを書くことで、アプリケーションの品質をあげることができる
  5. テストコードを書くことで、むしろ生産性をあげることができる

メリットしかないです🙌

初心者の方でも、フレームワーク使うならブラウザでUIを見ながら疎通確認・処理確認とか絶対にやめてください😅

理由としては簡単で、処理が n乗もしくはn倍で増えていくような場合は遅かれ早かれ圧倒的に自動テストに生産性が負けます。さらに、他の人がコードを読んだ時に「このコードの動作する条件はなんなのか? 仕様はなんだったのか?」といった情報がわからなくなります。

ぺん
ぺん
場合によっては、1ヶ月後の自分が見たときに、そのブラウザでガシャガシャやってた操作手順がわからなくなるぞw😂

作業と言いましたが、ひとまずLaravelを触ってみるならこのRepository~Modelの作成とテストを作業感が出てくるまで書いてみましょう。そうすればフレームワークの理解も深まるはずです。

NoteControllerを作成

[simterm]php artisan make:controller NoteController[/simterm]

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class NoteController extends Controller
{
    //
}

ぺん
ぺん
よし、とりあえず適当にCRUDコントローラ作るか。CreateとUpdateに関してはupsertで一気にやるから気にしない。
この記事を書いた人
この記事を書いた人
とりあえずざっくりレスポンスだけ書いたよ。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class NoteController extends Controller
{
    public function list(): JsonResponse
    {
        return response()->json('メモの一覧がここに入る');
    }

    public function save(): JsonResponse
    {
        return response()->json('メモの保存・アップデートの完了');
    }

    public function destroy(): JsonResponse
    {
        return response()->json('メモの削除');
    }
}

>> githubでコードを見る▲jsonレスポンスを返すコントローラ

ルーティング設定をしてControllerと疎通させる

ぺん
ぺん
既にフレームワーク作った時にサンプルのルーティングファイルはあるな。
この記事を書いた人
この記事を書いた人
今回はAPI作成なのでAPIルートファイルを参照します。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

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

>> githubでコードを見る▲./routes/api.php

この記事を書いた人
この記事を書いた人
サンプルのルートが疎通するか一応見ておきましょう。auth(認証)については設定してないので、適当なエラー画面が出てくるはず。(Not Foundではないことを確認)

http://localhost/api/user

APIなので/api/userをブラウザで確認してみましょう。

ぺん
ぺん
OK。APIは生きているっぽいな。
この記事を書いた人
この記事を書いた人
では実際にRoute設定を書いてみましょう。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\NoteController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

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

Route::get('/note', [NoteController::class, 'list']);

>> githubでコードを見る

ぺん
ぺん
よし、これで http://localhost/api/noteをGETすればさっきのControllerのlist()が見れるはず。どれどれ…
ぺん
ぺん
😭😭😭😭😭😭😭😭 
この記事を書いた人
この記事を書いた人
あ、アプリケーションのキャッシュが効いているようだね。ひとまずルーティング設定を追加したら下記コマンドをどうぞ。
[simterm]php artisan optimize //コンテナ内[/simterm]
ぺん
ぺん
あ、なんか謎の文字列が出てきたw 
この記事を書いた人
この記事を書いた人
これでひとまずコントローラーのlist()メソッドと/api/noteへのGETリクエストがつながりました。

他のAPIエンドポイントも追加していく


<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\NoteController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

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

Route::get('/note', [NoteController::class, 'list']);
Route::post('/note', [NoteController::class, 'save']);
Route::delete('/note', [NoteController::class, 'destroy']);

>> githubでコードを見る

この記事を書いた人
この記事を書いた人
OK。これでかけた。うーむ、まあGETはブラウザアクセスできるけどPOSTとかDELETEの検証めんどいなあ。
ぺん
ぺん
この辺りでテスト書いとくか 

コントローラとルーティングまでで疎通テスト

この記事を書いた人
この記事を書いた人
とりあえず手動で./tests/Feature/Controler フォルダを作成。NoteControllerTest.phpを作成してみました。

<?php

namespace Tests\Feature\Controllers;

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

class NoteControllerTest extends TestCase
{
  use RefreshDatabase;
}

ぺん
ぺん
じゃあさっきのルーティング全部が疎通してるか確認か。
この記事を書いた人
この記事を書いた人
とりあえずそれぞれの登録したルーティングAPIエンドポイントが疎通している(ステータス200番を返す)ことが必要っと…

<?php

namespace Tests\Feature\Controllers;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Http\Controller\NoteController;

class NoteControllerTest extends TestCase
{
  use RefreshDatabase;

  public function testList()
  {
    $response = $this->get('/api/note');

    $response->assertStatus(200);
  }

  public function testSave()
  {
    $response = $this->post('/api/note');

    $response->assertStatus(200);
  }

  public function testDestroy()
  {
    $response = $this->delete('/api/note');

    $response->assertStatus(200);
  }
}

>> githubでコードを見る

この記事を書いた人
この記事を書いた人
OK疎通してるっぽい。
ぺん
ぺん
じゃあ次はさっそくRepositoryをController上で呼んでみるか

ControllerにRepositoryを注入(DI)する


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Repositories\NoteRepository;

class NoteController extends Controller
{    
    /**
     * @var NoteRepository
     */
    private $noteRepository;
        
    /**
     * @param  NoteRepository $noteRepository
     */
    public function __construct(NoteRepository $noteRepository)
    {
        $this->noteRepository = $noteRepository;
    }
    
    /**
     * GET メモの一覧
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function list(Request $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->list()
        );
    }

    /**
     * POST メモの保存
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function save(Request $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->upsert($request->all())
        );
    }

    /**
     * DELETE メモの削除
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function destroy(Request $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->destroy($request->id)
        );
    }
}

>> githubでコードを見る

ぺん
ぺん
Laravelはやっぱりおまじないみたいに、コンストラクタインジェクション(コンストラクタで使いたいクラスを入れる)したら依存性解決してくれるから楽だな😂 これでさっきのテスト通したらどうなる?
この記事を書いた人
この記事を書いた人
想定どおり、idが必要なsave()とdestroy()でエラーが出たね。
ぺん
ぺん
テストコードが追いついていないから、修正・追加していこう

ControllerのテストもDIに伴って変更


<?php

namespace Tests\Feature\Controllers;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Http\Controller\NoteController;
use App\Models\Note;

class NoteControllerTest extends TestCase
{
  use RefreshDatabase;

  public function testList()
  {
    $response = $this->get('/api/note');

    $response->assertStatus(200);
  }

  public function testCreateOfSave()
  {
    // 新規作成用のデータでPOST
    $response = $this->post('/api/note', [
      'id' => null,
      'note_contents' => 'new note'
    ]);

    // 新規作成が成功する
    $response->assertStatus(200);
  }

  public function testUpdateOfSave()
  {
    // テスト用レコード作成
    $note = Note::factory()->create();

    // アップデート用のデータでPOST
    $response = $this->post('/api/note', [
      'id' => $note->id,
      'note_contents' => $note->note_contents
    ]);

    // アップデートが成功する
    $response->assertStatus(200);
  }

  public function testDestroy()
  {
    // テスト用レコード作成
    $note = Note::factory()->create();

    // 削除できる
    $response = $this->delete('/api/note', [
      'id' => $note->id
    ]);

    // 削除が成功する
    $response->assertStatus(200);
  }
}

>> githubでコードを見る

ぺん
ぺん
OK。ひとまずこれでバックエンドAPIの開発は完了かな。
この記事を書いた人
この記事を書いた人
あ、そうだ。140字以内っていう制限もあったから、一応、FormRequestのバリデーションは抑えておこう

スポンサードサーチ

FormRequestによるバリデーション

ぺん
ぺん
これについてはLaravelのフォームリクエストによるバリデーションになるかな。
この記事を書いた人
この記事を書いた人
うん。とりあえず内容を「140字以下」の時だけDB保存しないとね。DBのカラムが最大長だ255字だから、仮に1000行コピペして渡されても受け取れないしね。

既にコントローラーのテストがあるので、ガシガシと書いたコードの上に載せちゃいましょう。フォームリクエストを乗せてテストを回して異常が見つかれば修正していきます。

フォームリクエストのスケルトン作成

[simterm]php artisan make:request NoteRequest[/simterm]

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class NoteRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

▲./App/Http/Requests/NoteRequest.php

フォームリクエストの有効化


<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class NoteRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; //ユーザーがリクエストを行うことができるか
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'id' => 'nullable|numeric', // notes.idはnullもしくは数字(numeric)である
            'note_contents' => 'max:140' //note_contentsの最大長は140字
        ];
    }

    /**
     * Get the error messages for the defined validation rules.
     *
     * @return array
     */
    public function messages()
    {
        return [
            'note_contents.max' => '本文は140字以下にしてください。', //note_contentsのmaxバリデーションの際のエラーメッセージ
        ];
    }
}

▲./App/Http/Requests/NoteRequest.php

ぺん
ぺん
とりあえずコントローラーのRequestと入れ替えてテストしてみるか

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Repositories\NoteRepository;
use App\Http\Requests\NoteRequest;

class NoteController extends Controller
{    
    /**
     * @var NoteRepository
     */
    private $noteRepository;
        
    /**
     * @param  NoteRepository $noteRepository
     */
    public function __construct(NoteRepository $noteRepository)
    {
        $this->noteRepository = $noteRepository;
    }
    
    /**
     * GET メモの一覧
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function list(Request $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->list()
        );
    }

    /**
     * POST メモの保存
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function save(NoteRequest $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->upsert($request->all())
        );
    }

    /**
     * DELETE メモの削除
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function destroy(NoteRequest $request): JsonResponse
    {
        return response()->json(
            $this->noteRepository->destroy($request->id)
        );
    }
}

▲./App/Http/Controllers/NoteController.php

この記事を書いた人
この記事を書いた人
よし、とりあえずControllerのRequestをさっき書いたフォームリクエストに変えたよ。

>> githubのコミットはこちら 

 

▲テストしたら通る

ぺん
ぺん
まあ、テストコードでnote_contentsが「hoge」とか「huga」しかないから、140字かどうかの検証はできてないよなw ひとまずControllerとRequestの疎通は問題ないってだけで
この記事を書いた人
この記事を書いた人
なので、この作成したFormRequestのUnitテストも忘れず書きます

【テスト発展編】FormRequestのUnitテストを書く

ぺん
ぺん
FormRequestのテストを書いてみた…けどこれは初心者の方にやらせる内容じゃないな😂😅
この記事を書いた人
この記事を書いた人
ごめんなさい…ちゃんとFormRequestのUnitテストを書くにはバリデーターインスタンスを取って来たり、FormRequestクラスにrules()を差して置いたり、データプロバイダーを使ったりとテストの発展編になってしまいました🙏

一応、それぞれ実務では要求される範囲ではあるので、コードリーディングは行ってみてください。フォームリクエストのテストは基本的にこの形に乗っておけばどれも同じです😌

FormRequestのテストのスケルトンを手動で作成


<?php

namespace Tests\Unit\Requests;

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

class NoteRequestTest extends TestCase
{
  //
}

▲./tests/Unit/Requests/NoteRequestTest.php

作成したNoteRequestのUnitテストを作成

スケルトンに追記していきます。


<?php

namespace Tests\Unit\Requests;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Http\Requests\NoteRequest;
use Illuminate\Support\Str;

class NoteRequestTest extends TestCase
{
  use RefreshDatabase;

  public function setUp(): void
  {
    parent::setUp();
    $this->validator = app()->get('validator'); //バリデータインスタンス
    $this->rules = (new NoteRequest())->rules(); //フォームリクエストにrules()を差し込んでおく
  }
  
  /**
   * testAuthorize
   *
   * FormRequestが現在のユーザー権限で実行できるか
   * (今回は全てに適用するので常にtrueだが、稀にfalseだったりするのでテストしておく)
   */
  public function testAuthorize()
  {
    $this->assertTrue(
      (new NoteRequest())->authorize()
    );
  }

  /**
   * @test
   * @dataProvider validationProvider
   * 
   * テストの期待値
   * @param bool $shouldPass
   * 
   * フォームリクエストのモックデータ
   * @param array $mockedRequestData
   */
  public function バリデーションが通るか(bool $shouldPass, array $mockedRequestData)
  {
      $this->assertEquals(
          $shouldPass, 
          $this->validate($mockedRequestData)
      );
  }
  
  /**
   * テストの期待値: passed
   * フォームリクエストのモックデータ: data
   */
  public function validationProvider()
  {
      return [
          'アップデート: バリデーションが透る' => [
              'passed' => true,
              'data' => [
                  'id' => 1,
                  'note_contents' => '140字以下'
              ]
          ],
          'アップデート: バリデーションが通らない' => [
              'passed' => false,
              'data' => [
                'id' => 1,
                'note_contents' => Str::random(141) //141字のランダムな文字列を作成
              ]
          ],
          '新規作成: バリデーションが透る' => [
              'passed' => true,
              'data' => [
                  'id' => null,
                  'note_contents' => '140字以下'
              ]
          ],
          '新規作成: バリデーションが通らない' => [
              'passed' => false,
              'data' => [
                  'title' => null,
                  'note_contents' => Str::random(141) //141字のランダムな文字列を作成
              ]
          ]
      ];
  }
  
  /**
   * ルールを受け取って、バリデーターを実際に動作させる
   *
   * @param  array $mockedRequestData
   */
  protected function validate(array $mockedRequestData)
  {
      return $this->validator
          ->make($mockedRequestData, $this->rules)
          ->passes();
  }
}

▲./tests/Unit/Requests/NoteRequestTest.php

>> githubのコードはこちら

▲テストは緑でした

ぺん
ぺん
よし、これで一通りのUnitテストとFeatureテストが揃ってテストが通っているな。気持ちいな〜〜。お疲れ様。
この記事を書いた人
この記事を書いた人
フォームリクエストのテストは少し難しかったかもしれませんが…ひとまずこんな感じでしょうか? お疲れ様でした!

まとめ:バックエンドAPI編

これでバックエンドAPI編のチュートリアルは終わりです。

いかがでしたでしょうか?

この記事を書いた人
この記事を書いた人
よくあるチュートリアルと毛色が違う感じではありますが、一応、業務での手順を最も簡単にしていくとこんな感じになるかな?と思って作ってみました。
ぺん
ぺん
最近だと、業務未経験の人もPHPUnitなどテストまでかける必要があるらしいけど、なかなか実際のワークフローがわかっていない人も多いんじゃないかと思って、あえて手順は冗長にしてみたよ。

もし、不明な点などあればお気軽にコメントくださいませ😌

ABOUT ME
クスハラ
立命館大卒('18)後、ニート。 その後、webエンジニアとして独立・開業。 留学経験あり。TOEIC960点(大学3年時) 複数のwebサービスのグロースから開発までを担当。楽しく仕事して自由に生きているので、ブロガーとしては世の中の当たり前を疑う視点を提供する記事を書きたい。 ■連絡先 shogo.kusuhara[あっと]gmail.com

コメントはこちら

メールアドレスが公開されることはありません。 が付いている欄は必須項目です