LaraveでCRUD処理をクリーン・アーキテクチャで書いてみて、単体(Unit)テスト、機能(Feature)テストも書いてみる。

# 目次
- [概要](#概要)
- [開発環境](#開発環境)
- [環境構築](#環境構築)
- [Book プログラムの構成](#book-プログラムの構成)
- [本体プログラムのコード](#本体プログラムのコード)
- [単体（Unit）テストのコード](#単体unitテストのコード)
- [機能（Feature）テストのコード](#機能featureテストのコード)

# 概要
本（Book）のCRUD処理の **Web版** と **API版** を Laravelを使って、クリーン・アーキテクチャ で書いてみました。
環境構築には、Laravel Sail を使います。

# 開発環境
OS
- Windows11上のWSL2で動作するUbuntu

以下がインストールされている事
- Composer v2以上

# 環境構築
```shell
# Laravel をインストール（プロジェクト名：myapp）
$ composer create-project --prefer-dist laravel/laravel myapp

# Laravel Sail をインストール
$ cd myapp
$ composer require laravel/sail --dev

# Laravel Sail の設定。必要なサービスをSpaceキーで選択してEnter（※mysql, redis を選択しておく）
$ php artisan sail:install
 ┌ Which services would you like to install? ───────────────────┐
 │ › ◻ mysql                                                  ┃ │
 │   ◻ pgsql                                                  │ │
 │   ◻ mariadb                                                │ │
 │   ◻ mongodb                                                │ │
 │   ◻ redis                                                  │ │
 └────────────────────────────────────────────────── 0 selected ┘
```

.envを以下のように変更します。
```shell:.env
　：
APP_LOCALE=ja          # jaに変更
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=ja_JP # ja_JPに変更
　：
SESSION_DRIVER=redis   # redisに変更
　：
CACHE_DRIVER=redis     # 追加
　：
```

Laravel Sail では最初からテスト用の DB「testing」が用意されているので、.env を基にテスト用の環境設定ファイルを作成します。

```shell
$ cp .env .env.testing
```

.env.testing の内容を以下のように変更します。
```shell:.env.testing
APP_ENV=testing        # testingに変更
DB_DATABASE=testing    # testingに変更
```

phpunit.xml も以下のように修正します。

```xml:phpunit.xml
<env name="DB_DATABASE" value=":memory:"/>
                                 ↓
<env name="DB_DATABASE" value="testing"/>
```

Laravel Sail を起動します。

```shell
$ ./vendor/bin/sail up -d
```

DB のマイグレーションを実施します。

```shell
$ ./vendor/bin/sail artisan migrate               # ローカル用
$ ./vendor/bin/sail artisan migrate --env=testing # テスト用
```

ブラウザで http://localhost/ にアクセスし、Laravel の画面が表示できれば成功です。

# Book プログラムの構成
クリーンアーキテクチャの各層と、ディレクトリの関係は以下です。
```shell
app/
　 ├─ Model/
　 │　└── Book.php .....※1
　 │  
　 ├─ Domain/
　 │　├── Entities/ 　　　　　　　　　← (1)エンティティ層（ロジックがあれば、Unit テスト対象：今回は無し）
　 │　│　　└── Book.php .....※1
　 │　└── Repositories/
　 │　　　 └── BookRepositoryInterface.php .....※2
　 │  
　 ├─ Application/
　 │　└── UseCases/　　　　　　　　　 ← (2)ユースケース層（Unit テスト対象）
　 │　　　 ├─ CreateBookUseCase.php
　 │　　　 ├─ UpdateBookUseCase.php
　 │　　　 ├─ DeleteBookUseCase.php
　 │　　　 ├─ GetBookListUseCase.php
　 │　　　 └─ GetBookUseCase.php
　 │
　 ├─ Infrastructure/　　　　　　　　← (4)フレームワーク＆ドライバ層（Feature テスト対象）
　 │　└── Repositories/
　 │　　　 └─ BookRepository.php
　 │ 
　 ├─ DTOs/
　 │　└──  BookDto.php
　 │ 
　 ├─ Http/
　 │  └─ Controllers/　　　　　　　　← (3)インターフェース アダプタ層（Feature テスト対象）
　 │　　　 ├─ WebBookController.php
　 │　　　 └─ ApiBookController.php
　 │
　 ├─ routes/　　　　　　　　　　　　 ← (3)インターフェース アダプタ層（Feature テスト対象）
 　│　　　 ├── web.php
　 │　　　 └── api.php     
```
## 何故 Book.php（※1）が2つ存在するのか？
Laravelで Model と Entity に同じ名前の Book.php が存在するのは、**責任と関心の分離** を明確にするためです。目的がまったく異なるため、名前が同じでも役割は異なります。

**■ App\Models\Book**
Laravelの Eloquent ORM が提供する データベースとのやり取りのためのクラス。
- テーブルとマッピングされている（例：books テーブル）
- DBとのCRUD操作ができる
- Model::find(), Model::where(), save() などが使える
- データベースアクセスできる「永続化層」

**■ App\Domain\Entities\Book**
クリーンアーキテクチャでいうところの、純粋なドメインオブジェクト。
- ビジネスルールや振る舞い（メソッド）を内包
- LaravelのEloquentとは無関係
- データベースやフレームワークに依存しない
- ビジネスロジックの中核


## 何故「BookRepositoryInterface（※2）」は BookRepository.php と同じ Infrastructure ディレクトリではないのか？
Infrastructure ディレクトリ は Laravel や DB（Eloquent）などの 外部技術に依存している層です。
一方、BookRepositoryInterface はアプリケーションやドメインの コアロジック（＝内側）で使われ、どんな DB でも動くように中立な定義でなければなりません。
そのような理由から「インターフェースは外部に依存してはいけない」＝ Infrastructure には置かないのです。

# 本体プログラムのコード

## Bookモデルとマイグレーション作成

以下コマンドで Book モデルとマイグレーションを作成します。
```shell
$ php artisan make:model Book -m
```

Bookモデルはの内容は以下となります。
```php:app/Models/Book.php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = ['title', 'author', 'description']; // 一括代入(fillable)を設定
}
```
マイグレーションファイルの内容は以下となります。
```php:app/database/migrations/xxxx_xx_xx_xxxxxx_create_books_table.php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};
```

以下コマンドで、Bookテーブルのマイグレーションを実施します。

```shell
$ ./vendor/bin/sail artisan migrate               # ローカル用
$ ./vendor/bin/sail artisan migrate --env=testing # テスト用
```

## DTOの作成
DTO（Data Transfer Object）の役割は、 **「層（レイヤー）間でデータを運ぶ専用のオブジェクト」** です。クリーンアーキテクチャにおいて、エンティティやリクエストの生データと切り離して、シンプルかつ明確にデータのやり取りを行うことが目的です。
```shell:
$ mkdir app/DTOs
$ touch app/DTOs/BookDto.php
```
DTOの内容は以下となります。
```php:app/DTOs/BookDto.php
<?php
namespace App\DTOs;

class BookDto
{
    public function __construct(
        public readonly ?int $id,
        public readonly string $title,
        public readonly string $author,
        public readonly ?string $description,
    ) {}
}
```

## (1) のエンティティ層の作成
```shell
$ mkdir -p app/Domain/Entities
$ touch app/Domain/Entities/Book.php
```
内容は以下となります。
```php:app/Domain/Entities/Book.php
<?php
namespace App\Domain\Entities;

class Book
{
    public function __construct(
        private int $id,
        private string $title,
        private string $author,
        private ?string $description,
    ) {}

    public function getId(): int { return $this->id; }
    public function getTitle(): string { return $this->title; }
    public function getAuthor(): string { return $this->author; }
    public function getDescription(): string | null { return $this->description; }
}
```

## (4) のフレームワーク＆ドライバ層（実装）と、そのインターフェース（I/F）の作成
```shell
$ mkdir -p app/Infrastructure/Repositories
$ touch app/Infrastructure/Repositories/BookRepository.php
```

実装の内容は以下となります。

```php:app/Infrastructure/Repositories/BookRepository.php
<?php
namespace App\Infrastructure\Repositories;

use App\Domain\Entities\Book;
use App\Models\Book as EloquentBook;
use App\DTOs\BookDto;
use App\Domain\Repositories\BookRepositoryInterface;

class BookRepository implements BookRepositoryInterface
{
    public function getById(int $id): ?Book
    {
        $record = EloquentBook::find($id);
        if (!$record) return null;

        return new Book(
            $record->id,
            $record->title,
            $record->author,
            $record->description
        );
    }

    public function getAll(): array
    {
        return EloquentBook::all()
            ->map(fn($record) => new BookDto($record->id, $record->title, $record->author, $record->description))
            ->toArray();
    }

    public function save(BookDto $book): BookDto
    {
        $record = $book->id ? EloquentBook::find($book->id) : new EloquentBook();
        $record->title = $book->title;
        $record->author = $book->author;
        $record->description = $book->description;
        $record->save();

        return new BookDto($record->id, $record->title, $record->author, $record->description);
    }

    public function delete(int $id): void
    {
        EloquentBook::destroy($id);
    }
}
```

I/Fを作成します。

```shell
$ mkdir -p app/Domain/Repositories
$ touch app/Domain/Repositories/BookRepositoryInterface.php
```

I/Fの内容は以下となります。

```php:app/Domain/Repositories/BookRepositoryInterface.php
<?php

namespace App\Domain\Repositories;

use App\Domain\Entities\Book;
use App\DTOs\BookDto;

interface BookRepositoryInterface
{
    public function getById(int $id): ?Book;

    public function getAll(): array;

    public function save(BookDto $book): BookDto;

    public function delete(int $id): void;
}
```

## (2) のユースケース層の作成
```shell
$ mkdir app/Application/UseCases
$ touch app/Application/UseCases/GetBookListUseCase.php
$ touch app/Application/UseCases/GetBookUseCase.php
$ touch app/Application/UseCases/CreateBookUseCase.php
$ touch app/Application/UseCases/UpdateBookUseCase.php
$ touch app/Application/UseCases/DeleteBookUseCase.php
```
GetBookListUseCase.php の内容は以下となります。
```php:app/Application/UseCases/GetBookListUseCase.php
<?php
namespace App\Application\UseCases;

use App\Domain\Repositories\BookRepositoryInterface;

class GetBookListUseCase
{
    public function __construct(
        private BookRepositoryInterface $bookRepository
    ) {}

    public function execute(): array
    {
        return $this->bookRepository->getAll();
    }
}
```

GetBookUseCase.php の内容は以下となります。

```php:app/Application/UseCases/GetBookUseCase.php
<?php
namespace App\Application\UseCases;

use App\Domain\Repositories\BookRepositoryInterface;
use App\DTOs\BookDto;

class GetBookUseCase
{
    public function __construct(
        private BookRepositoryInterface $bookRepository
    ){}

    public function execute(int $id): ?BookDto
    {
        $book = $this->bookRepository->getById($id);
        if (!$book) return null;
        return new BookDto($book->getId(), $book->getTitle(), $book->getAuthor(), $book->getDescription());
    }
}
```

CreateBookUseCase.php の内容は以下となります。

```php:app/Application/UseCases/CreateBookUseCase.php
<?php
namespace App\Application\UseCases;

use App\Domain\Repositories\BookRepositoryInterface;
use App\DTOs\BookDto;
use App\Domain\Entities\Book;

class CreateBookUseCase
{
    public function __construct(
        private BookRepositoryInterface $bookRepository
    ){}

    public function execute(string $title, string $author, ?string $description): BookDto
    {
        $bookDto = new BookDto(null, $title, $author, $description);
        return $this->bookRepository->save($bookDto);
    }
}
```

UpdateBookUseCase.php の内容は以下となります。

```php:app/Application/UseCases/UpdateBookUseCase.php
<?php
namespace App\Application\UseCases;

use App\Domain\Repositories\BookRepositoryInterface;
use App\DTOs\BookDto;

class UpdateBookUseCase
{
    public function __construct(
        private BookRepositoryInterface $bookRepository
    ){}

    public function execute(int $id, string $title, string $author, ?string $description): ?BookDto
    {
        $book = $this->bookRepository->getById($id);
        if (!$book) return null;

        $bookDto = new BookDto($id, $title, $author, $description);
        return $this->bookRepository->save($bookDto);
    }
}
```

DeleteBookUseCase.php の内容は以下となります。

```php:app/Application/UseCases/DeleteBookUseCase.php
<?php
namespace App\Application\UseCases;

use App\Domain\Repositories\BookRepositoryInterface;

class DeleteBookUseCase
{
    public function __construct(
        private BookRepositoryInterface $bookRepository
    ){}

    public function execute(int $id): void
    {
        $this->bookRepository->delete($id);
    }
}
```

## (3) のインターフェース アダプタ層の作成

ここでは、Web用と、API用の２つを作成します。
インターフェースアダプタ層さえ変えれば、上位のエンティティ層やユースケース層は変更しなくても利用できます。

まずはWeb用のコントローラーを作成します。
```shell
$ php artisan make:controller WebBookController --resource # CRUDメソッドも自動作成
```

Web用コントローラーの内容は以下となります。

```php:app/Http/Controllers/WebBookController.php
<?php
namespace App\Http\Controllers;

use App\Application\UseCases\CreateBookUseCase;
use App\Application\UseCases\UpdateBookUseCase;
use App\Application\UseCases\DeleteBookUseCase;
use App\Application\UseCases\GetBookUseCase;
use App\Application\UseCases\GetBookListUseCase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class WebBookController extends Controller
{
    private GetBookListUseCase $getListUseCase;
    private GetBookUseCase $getUseCase;
    private CreateBookUseCase $createUseCase;
    private UpdateBookUseCase $updateUseCase;
    private DeleteBookUseCase $deleteUseCase;
    
    public function __construct(
        GetBookListUseCase $getListUseCase,
        GetBookUseCase $getUseCase,
        CreateBookUseCase $createUseCase,
        UpdateBookUseCase $updateUseCase,
        DeleteBookUseCase $deleteUseCase,
    ) {
        $this->getListUseCase = $getListUseCase;
        $this->getUseCase = $getUseCase; 
        $this->createUseCase = $createUseCase;
        $this->updateUseCase = $updateUseCase;
        $this->deleteUseCase = $deleteUseCase;
    }

    public function index()
    {
        $books = $this->getListUseCase->execute();
        return view('books.index', compact('books'));
    }

    public function show(int $id)
    {
        $book = $this->getUseCase->execute($id);
        if (!$book) {
            return response("本が見つかりません", 404);
            // return redirect()->route('books.index')->with('error', '本が見つかりません');
        }
        return view('books.show', compact('book'));
    }

    public function create()
    {
        return view('books.create');
    }

    public function store(Request $request)
    {
        // 入力バリデーション：エラー時には自動的に元のフォームへリダイレクト
        $validated = $request->validate([
            'title' => 'required|string|min:3',
            'author' => 'required|string|min:3',
            'description' => 'nullable|string',
        ]);

        try{
            $this->createUseCase->execute($validated['title'], $validated['author'], $validated['description']);
        } catch (\InvalidArgumentException $e) {
            return redirect()->back()->withErrors(['error' => $e->getMessage()]);
        }

        return redirect()->route('books.index')->with('success', '本を作成しました');
    }

    public function edit(int $id)
    {
        $book = $this->getUseCase->execute($id);
        if (!$book) {
            return redirect()->route('books.index')->with('error', '本が見つかりません');
        }

        return view('books.edit', compact('book'));
    }

    public function update(Request $request, int $id)
    {
        $validated = $request->validate([
            'title' => 'required|string|min:3',
            'author' => 'required|string|min:3',
            'description' => 'nullable|string',
        ]);

        try{
            $book = $this->updateUseCase->execute($id, $validated['title'], $validated['author'], $validated['description']);
            if (!$book) {
                return redirect()->route('books.index')->with('error', 'Book not found');
            }
        } catch (\InvalidArgumentException $e) {
            return redirect()->back()->withErrors(['error' => $e->getMessage()]);
        }

        return redirect()->route('books.index')->with('success', '本を更新しました');
    }

    public function destroy(int $id)
    {
        $this->deleteUseCase->execute($id);
        return redirect()->route('books.index')->with('success', '本を削除しました');
    }
}
```

Webのルーティングを設定します。

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;

Route::get('/books', [WebBookController::class, 'index'])->name('books.index');
Route::get('/books/create', [WebBookController::class, 'create'])->name('books.create');
Route::get('/books/{id}/edit', [WebBookController::class, 'edit'])->name('books.edit');
Route::post('/books', [WebBookController::class, 'store'])->name('books.store');;
Route::get('/books/{id}', [WebBookController::class, 'show'])->name('books.show');
Route::put('/books/{id}', [WebBookController::class, 'update'])->name('books.update');
Route::delete('/books/{id}', [WebBookController::class, 'destroy'])->name('books.destroy');
```

次にAPI用のコントローラーを作成します。
```shell
$ php artisan make:controller ApiBookController --resource # CRUDメソッドも自動作成
```

API用コントローラーの内容は以下となります。
```php:app/Http/Controllers/ApiBookController.php
<?php
namespace App\Http\Controllers;

use App\Application\UseCases\CreateBookUseCase;
use App\Application\UseCases\UpdateBookUseCase;
use App\Application\UseCases\DeleteBookUseCase;
use App\Application\UseCases\GetBookUseCase;
use App\Application\UseCases\GetBookListUseCase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ApiBookController extends Controller
{
    private GetBookListUseCase $getListUseCase;
    private GetBookUseCase $getUseCase;
    private CreateBookUseCase $createUseCase;
    private UpdateBookUseCase $updateUseCase;
    private DeleteBookUseCase $deleteUseCase;
    
    public function __construct(
        GetBookListUseCase $getListUseCase,
        GetBookUseCase $getUseCase,
        CreateBookUseCase $createUseCase,
        UpdateBookUseCase $updateUseCase,
        DeleteBookUseCase $deleteUseCase,
        
        
    ) {
        $this->getListUseCase = $getListUseCase;
        $this->getUseCase = $getUseCase; 
        $this->createUseCase = $createUseCase;
        $this->updateUseCase = $updateUseCase;
        $this->deleteUseCase = $deleteUseCase;
    }

    public function index()
    {
        $books = $this->getListUseCase->execute();
        return response()->json($books);
    }

    public function show(int $id)
    {
        $book = $this->getUseCase->execute($id);
        if (!$book) return response()->json(['message' => '本が見つかりません'], 404);
        return response()->json($book);
    }

    public function store(Request $request)
    {
        try {
            // 入力バリデーション
            $validator = Validator::make($request->all(), [
                'title' => 'required|string|min:3',
                'author' => 'required|string|min:3',
                'description' => 'nullable|string',
            ]);

            if ($validator->fails()) {
                return response()->json(['errors' => $validator->errors()], 422);
            }

            $validated = $validator->validated();

            $book = $this->createUseCase->execute(
                $validated['title'],
                $validated['author'],
                $validated['description'] ?? null
            );

            return response()->json($book, 201);

        } catch (\InvalidArgumentException $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        } catch (\Throwable $e) {
            return response()->json(['error' => 'Unexpected error'], 500);
        }
    }

    public function update(Request $request, int $id)
    {
        try {
            // 入力バリデーション
            $validator = Validator::make($request->all(), [
                'title' => 'required|string|min:3',
                'author' => 'required|string|min:3',
                'description' => 'nullable|string',
            ]);

            if ($validator->fails()) {
                return response()->json(['errors' => $validator->errors()], 422);
            }

            $validated = $validator->validated();

            $book = $this->updateUseCase->execute(
                $id,
                $validated['title'],
                $validated['author'],
                $validated['description'] ?? null
            );
            if (!$book) {
                return response()->json(['message' => '本が見つかりません'], 404);
            }
            return response()->json($book, 201);

        } catch (\InvalidArgumentException $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        } catch (\Throwable $e) {
            return response()->json(['error' => 'Unexpected error'], 500);
        }
    }

    public function destroy(int $id)
    {
        $this->deleteUseCase->execute($id);
        return response()->json(null, 204);
    }
}
```

APIのルーティングを設定します。

```php:routes/api.php
<?php

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

Route::get('/books', [ApiBookController::class, 'index']);
Route::post('/books', [ApiBookController::class, 'store']);
Route::get('/books/{id}', [ApiBookController::class, 'show']);
Route::put('/books/{id}', [ApiBookController::class, 'update']);
Route::delete('/books/{id}', [ApiBookController::class, 'destroy']);
```



## サービスプロバイダでバインド

BookRepositoryInterface を必要としている場所には、BookRepository を渡すように Laravel に教えます。
```php:app/Providers/AppServiceProvider.php
use App\Domain\Repositories\BookRepositoryInterface; // 追加
use App\Infrastructure\Repositories\BookRepository; // 追加
    ：
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(BookRepositoryInterface::class, BookRepository::class); // 追加
    }
　　：
```

## Web用 Viewファイルの作成（必要最小限）

View 用 のディレクトリと、該当ファイルを作成します。

```shell
$ mkdir -p resources/views/books
$ touch resources/views/books/index.blade.php
$ touch resources/views/books/create.blade.php
$ touch resources/views/books/show.blade.php
$ touch resources/views/books/edit.blade.php
```

```php:resources/views/books/index.blade.php
<h1>本の一覧</h1>

@if (session('success'))
    <div style="color:green;">
        {{ session('success') }}
    </div>
@endif
@if (session('error'))
    <div style="color:red;">
        {{ session('error') }}
    </div>
@endif

<a href="{{ route('books.create') }}">新規作成</a>

<ul>
    @foreach ($books as $book)
        <li>
            <a href="{{ route('books.show', $book->id) }}">{{ $book->title }}</a>
            <a href="{{ route('books.edit', $book->id) }}">[編集]</a>
            <form action="{{ route('books.destroy', $book->id) }}" method="POST" style="display:inline">
                @csrf
                @method('DELETE')
                <button type="submit">削除</button>
            </form>
        </li>
    @endforeach
</ul>
```

```php:resources/views/books/create.blade.php
<h1>本の新規作成</h1>
@if ($errors->has('error'))
    <div style="color:red;">
        {{ $errors->first('error') }}
    </div>
@endif

<form method="POST" action="{{ route('books.store') }}">
    @csrf

    <div>
        <label>タイトル:</label><br>
        @if ($errors->has('title'))
            <div style="color:red;">
                {{ $errors->first('title') }}
            </div>
        @endif
        <input type="text" name="title" value="{{ old('title') }}">
    </div>

    <div>
        <label>著者:</label><br>
        @if ($errors->has('author'))
            <div style="color:red;">
                {{ $errors->first('author') }}
            </div>
        @endif
        <input type="text" name="author" value="{{ old('author') }}">
    </div>

    <div>
        <label>説明:</label><br>
        <textarea name="description" rows="4">{{ old('description') }}</textarea>
    </div>

    <button type="submit">登録</button>
</form>

<a href="{{ route('books.index') }}">← 一覧に戻る</a>
```

```php:resources/views/books/show.blade.php
<h1>本の詳細</h1>

<p><strong>タイトル:</strong> {{ $book->title }}</p>
<p><strong>著者:</strong> {{ $book->author }}</p>
<p><strong>説明:</strong> {{ $book->description }}</p>

<a href="{{ route('books.edit', $book->id) }}">編集</a>
<a href="{{ route('books.index') }}">一覧に戻る</a>
```

```php:resources/views/books/edit.blade.php
<h1>本の編集</h1>
@if ($errors->has('error'))
    <div style="color:red;">
        {{ $errors->first('error') }}
    </div>
@endif

<form method="POST" action="{{ route('books.update', $book->id) }}">
    @csrf
    @method('PUT')

    <div>
        <label>タイトル:</label><br>
        <input type="text" name="title" value="{{ $book->title }}">
    </div>

    <div>
        <label>著者:</label><br>
        <input type="text" name="author" value="{{ $book->author }}">
    </div>

    <div>
        <label>説明:</label><br>
        <textarea name="description" rows="4">{{ $book->description }}</textarea>
    </div>

    <button type="submit">更新</button>
</form>

<a href="{{ route('books.index') }}">← 一覧に戻る</a>
```

## エラーメッセージの日本語化対応
lang/en ディレクトリをコピーして、lang/ja ディレクトリを作成します。
lang/ja/validation.php ファイルを修正します。

```shell
lang/
　 ├─ ja/
　 │　├─ auth.php
　 │　├─ pagination.php
　 │　├─ passwords.php
　 │　└─ validation.php   ← 修正するファイル
```

```php:lang/ja/validation.php
     :
     :
    'min' => [
        'array' => 'The :attribute field must have at least :min items.',
        'file' => 'The :attribute field must be at least :min kilobytes.',
        'numeric' => 'The :attribute field must be at least :min.',
        'string' => ':attributeは:min文字以上入力してください。',  ← 日本語にする
    ],
     :
     :
    'required' => ':attributeが入力されていません。',  ← 日本語にする
     :
     :
    /*
    |--------------------------------------------------------------------------
    | Custom Validation Attributes
    |--------------------------------------------------------------------------
    |
    | The following language lines are used to swap our attribute placeholder
    | with something more reader friendly such as "E-Mail Address" instead
    | of "email". This simply helps us make our message more expressive.
    |
    */

    'attributes' => [
        'title' => 'タイトル',　　 ← 追加する
        'author' => '著者',  　　　← 追加する
        'description' => '説明',　 ← 追加する
    ],
     :
```

Webの場合、ブラウザで http://localhost/books にアクセスして以下のような画面が出たら成功です。
![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/0dc202d9-52c2-4840-bf4f-1ee753698c71.png)
新規作成のリンクをクリックすると、以下のような画面になるので、色々データを入力してみてください。
![create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/561a38f4-3f5a-41f3-a9d1-6447395dbb3f.png)

APIの場合にはPostmanなどのクライアントソフトを使って、以下のようにGETリクエストするとトップページのJSONを取得します。
![postman_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/45bb0a01-9b98-41c9-bdb0-0df621fd4e8d.png)

新規作成の場合には、以下のようにリクエストJSONをセットしてPOST送信するとデータが登録されます。
こちらも色々データを入力してみてください。
![postman_create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/0b2b861e-b7f0-47d2-8c07-fbbe9124de75.png)

# 単体（Unit）テストのコード

ユニットテストなので、ユースケース層の **メソッド単位** のテストを行います。

## リポジトリ層のテストコード

以下のコマンドで ユースケース層の Unit テスト用ファイルの雛形を作成します。

```
$ php artisan make:test BookUseCaseTest --unit
```

作成された Unit テスト用のファイルを以下のようにしてください。

```php:tests/Unit/BookUseCaseTest.php
<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\Domain\Entities\Book;
use App\Application\UseCases\GetBookListUseCase;
use App\Application\UseCases\GetBookUseCase;
use App\Application\UseCases\CreateBookUseCase;
use App\Application\UseCases\UpdateBookUseCase;
use App\Application\UseCases\DeleteBookUseCase;
use App\Domain\Repositories\BookRepositoryInterface;
use App\DTOs\BookDto;
use Mockery;

class BookUseCaseTest extends TestCase
{
    public function tearDown(): void
    {
        Mockery::close(); // モックをクリーンアップ
        parent::tearDown();
    }

    public function test_Book一覧を取得()
    {
        // モックの作成
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $bookList = [
            new BookDto(1, 'タイトルA', '著者A', '説明A'),
            new BookDto(2, 'タイトルB', '著者B', '説明B'),
        ];

        $mockRepository->shouldReceive('getAll')
            ->once()
            ->andReturn($bookList);

        $useCase = new GetBookListUseCase($mockRepository);
        $result = $useCase->execute();

        $this->assertCount(2, $result);
        $this->assertEquals('タイトルA', $result[0]->title);
        $this->assertEquals('著者B', $result[1]->author);
    }

    public function test_Bookの詳細を取得()
    {
        // モックの作成
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $book = new Book(1, 'タイトルA', '著者A', '説明A');

        $mockRepository->shouldReceive('getById')
            ->once()
            ->andReturn($book);

        $useCase = new GetBookUseCase($mockRepository);
        $result = $useCase->execute(1);

        $this->assertEquals('タイトルA', $result->title);
    }

    public function test_Bookを新規登録(){
        // モックの作成
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $title = 'タイトルA';
        $author = '著者A';
        $description = '説明A';

        $expectedDto = new BookDto(1, $title, $author, $description);

        $mockRepository
            ->shouldReceive('save')
            ->once()
            ->with(Mockery::on(function ($arg) use ($title, $author, $description) {
                return $arg instanceof BookDto
                    && $arg->id === null
                    && $arg->title === $title
                    && $arg->author === $author
                    && $arg->description === $description;
            }))
            ->andReturn($expectedDto);

        $useCase = new CreateBookUseCase($mockRepository);
        $result = $useCase->execute($title, $author, $description);

        $this->assertInstanceOf(BookDto::class, $result);
        $this->assertEquals(1, $result->id);
        $this->assertEquals($title, $result->title);
        $this->assertEquals($author, $result->author);
        $this->assertEquals($description, $result->description);
    }

    public function test_Bookを更新（成功）()
    {
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $id = 1;
        $title = 'タイトルA';
        $author = '著者A';
        $description = '説明A';

        $book = new Book($id, $title, $author, $description);

        $mockRepository
            ->shouldReceive('getById')
            ->with($id)
            ->andReturn($book);

        $expectedDto = new BookDto($id, $title, $author, $description);

        $mockRepository
            ->shouldReceive('save')
            ->with(Mockery::on(function ($arg) use ($expectedDto) {
                return $arg instanceof BookDto &&
                       $arg->id === $expectedDto->id &&
                       $arg->title === $expectedDto->title &&
                       $arg->author === $expectedDto->author &&
                       $arg->description === $expectedDto->description;
            }))
            ->andReturn($expectedDto);

        $useCase = new UpdateBookUseCase($mockRepository);
        $result = $useCase->execute($id, $title, $author, $description);

        $this->assertInstanceOf(BookDto::class, $result);
        $this->assertEquals($title, $result->title);
        $this->assertEquals($author, $result->author);
        $this->assertEquals($description, $result->description);
    }

    public function test_Bookを更新（失敗）()
    {
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $id = 999;
        $title = 'タイトルA';
        $author = '著者A';
        $description = '説明A';

        $mockRepository
            ->shouldReceive('getById')
            ->with($id)
            ->andReturn(null);

        $useCase = new UpdateBookUseCase($mockRepository);
        $result = $useCase->execute($id, $title, $author, $description);

        $this->assertNull($result);
    }

    public function test_Bookを削除()
    {
        $mockRepository = Mockery::mock(BookRepositoryInterface::class);
        
        $id = 1;

        $mockRepository
            ->shouldReceive('delete')
            ->once()
            ->with($id);

        $useCase = new DeleteBookUseCase($mockRepository);
        $useCase->execute($id);

        // executeは何も返さないので、明示的に「1件アサーションを行った」と通知
        $this->addToAssertionCount(1);
    }
}
```

## 単体（Unit）テストの実行結果

```shell
$ ./vendor/bin/sail up -d # Laravel起動
$ ./vendor/bin/sail artisan test tests/Unit # Unitフォルダ内のみ指定してテスト実行

   PASS  Tests\Unit\BookUseCaseTest
  ✓ book一覧を取得                                                                                        0.09s
  ✓ bookの詳細を取得                                                                                       0.01s
  ✓ bookを新規登録                                                                                        0.01s
  ✓ bookを更新（成功）                                                                                      0.01s
  ✓ bookを更新（失敗）                                                                                      0.01s
  ✓ bookを削除                                                                                          0.01s

  Tests:    6 passed (15 assertions)
  Duration: 0.24s
```

# 機能（Feature）テストのコード

機能テストなので、インターフェース アダプタ層の **HTTPリクエスト単位** と、フレームワーク＆ドライバ層の **DBアクセス** のテストを行います。

## インターフェース アダプタ層のテストコード（Web用）

以下のコマンドで インターフェース アダプタ層（Web用）の Feature テスト用ファイルの雛形を作成します。

```
$ php artisan make:test WebBookControllerTest
```

作成された Feature テスト用のファイルを以下のようにしてください。

```php:tests/Feature/WebBookControllerTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Application\UseCases\GetBookListUseCase;
use App\Application\UseCases\GetBookUseCase;
use App\Application\UseCases\CreateBookUseCase;
use App\Application\UseCases\UpdateBookUseCase;
use App\Application\UseCases\DeleteBookUseCase;
use App\DTOs\BookDto;
use Mockery;

class WebBookControllerTest extends TestCase
{
    public function test_Indexアクション(): void
    {
        // モックとフェイクデータ
        $bookDtoA = new BookDto(1, 'タイトルA', '著者A', '説明A');
        $bookDtoB = new BookDto(2, 'タイトルB', '著者B', '説明B');
        $books = [$bookDtoA, $bookDtoB];

        $mockUseCase = Mockery::mock(GetBookListUseCase::class);
        $mockUseCase->shouldReceive('execute')->once()->andReturn($books);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookListUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookListUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get('/books');

        $response->assertStatus(200);
        $response->assertViewIs('books.index');
        $response->assertViewHas('books', $books);
    }

    public function test_Showアクション（成功）(): void
    {
        // モックとフェイクデータ
        $id = 1;
        $bookDto = new BookDto($id, 'タイトルA', '著者A', '説明A');

        $mockUseCase = Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->with($id)
            ->once()
            ->andReturn($bookDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/books/{$id}");

        $response->assertStatus(200);
        $response->assertViewIs('books.show');
        $response->assertViewHas('book', function ($viewBook) use ($bookDto) {
            return $viewBook === $bookDto;
        });
    }

    public function test_Showアクション（該当データなし）(){
        // モックとフェイクデータ
        $id = 999;

        $mockUseCase = Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->with($id)
            ->once()
            ->andReturn(null);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/books/{$id}");

        // $response->assertRedirect(route('books.index'));
        // $response->assertSessionHas('error', '本が見つかりません');
        $response->assertStatus(404);
        $response->assertSeeText('本が見つかりません');
    }

    public function test_Createアクション(){
        $response = $this->get('/books/create');

        $response->assertStatus(200);
        $response->assertViewIs('books.create');
    }

    public function test_Storeアクション（成功）(){
        // モックとフェイクデータ
        $title = 'タイトルA';
        $author = '著者A';
        $description = '説明A';

        $mockDto = new BookDto(1, $title, $author, $description);

        // ユースケースのモックを注入
        $mockUseCase = \Mockery::mock(CreateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($title, $author, $description)
            ->andReturn($mockDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する CreateBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(CreateBookUseCase::class, $mockUseCase);

        // POSTリクエスト
        $response = $this->post('/books', [
            'title' => $title,
            'author' => $author,
            'description' => $description,
        ]);

        $response->assertRedirect('/books');
        $response->assertSessionHas('success', '本を作成しました');
    }

    public function test_Storeアクション（バリデーションエラー：必須チェック）()
    {
        // POSTリクエスト
        $response = $this->post('/books', [
            'title' => '',
            'author' => '',
            'description' => '説明A',
        ]);

        $response->assertRedirect();

        $errors = session('errors');
        $this->assertEquals('タイトルが入力されていません。', $errors->first('title'));
        $this->assertEquals('著者が入力されていません。', $errors->first('author'));
    }

    public function test_Storeアクション（バリデーションエラー：文字数チェック）()
    {
        // POSTリクエスト
        $response = $this->post('/books', [
            'title' => 'A',
            'author' => 'A',
            'description' => '説明A',
        ]);

        $response->assertRedirect();

        $errors = session('errors');
        $this->assertEquals('タイトルは3文字以上入力してください。', $errors->first('title'));
        $this->assertEquals('著者は3文字以上入力してください。', $errors->first('author'));
    }

    public function test_Editアクション（成功）(){
        // モックとフェイクデータ
        $bookId = 1;
        $bookDto = new BookDto($bookId, 'タイトルA', '著者A', '説明A');

        $mockUseCase = \Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId)
            ->andReturn($bookDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/books/{$bookId}/edit");

        $response->assertStatus(200);
        $response->assertViewIs('books.edit');
        $response->assertViewHas('book', $bookDto);
    }

    public function test_Editアクション（該当データなし）(){
        // モックとフェイクデータ
        $bookId = 999;

        $mockUseCase = \Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId)
            ->andReturn(null);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/books/{$bookId}/edit");

        $response->assertRedirect('/books');
        $response->assertSessionHas('error', '本が見つかりません');
    }

    public function test_Updateアクション（成功）(){ 
        // モックとフェイクデータ
        $bookId = 1;
        $data = [
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ];
        $bookDto = new BookDto($bookId, $data['title'], $data['author'], $data['description']);

        $mockUseCase = \Mockery::mock(UpdateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId, $data['title'], $data['author'], $data['description'])
            ->andReturn($bookDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(UpdateBookUseCase::class, $mockUseCase);

        // PUTリクエスト
        $response = $this->put("/books/{$bookId}", $data);

        $response->assertRedirect('/books');
        $response->assertSessionHas('success',  '本を更新しました');
    }

    public function test_Updateアクション（バリデーションエラー：必須チェック）()
    {
        // PUTリクエスト
        $bookId = 1;
        $response = $this->put("/books/{$bookId}", [
            'title' => '',
            'author' => '',
            'description' => '説明A',
        ]);

        $response->assertRedirect();

        $errors = session('errors');
        $this->assertEquals('タイトルが入力されていません。', $errors->first('title'));
        $this->assertEquals('著者が入力されていません。', $errors->first('author'));
    }

    public function test_Updateアクション（バリデーションエラー：文字数チェック）()
    {
        // PUTリクエスト
        $bookId = 1;
        $response = $this->put("/books/{$bookId}", [
            'title' => 'A',
            'author' => 'A',
            'description' => '説明A',
        ]);

        $response->assertRedirect();

        $errors = session('errors');
        $this->assertEquals('タイトルは3文字以上入力してください。', $errors->first('title'));
        $this->assertEquals('著者は3文字以上入力してください。', $errors->first('author'));
    }

    public function test_Updateアクション（該当データなし）(){    
        // モックとフェイクデータ
        $bookId = 999;
        $data = [
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ];

        $mockUseCase = \Mockery::mock(UpdateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId, $data['title'], $data['author'], $data['description'])
            ->andReturn(null);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(UpdateBookUseCase::class, $mockUseCase);

        // PUTリクエスト
        $response = $this->put("/books/{$bookId}", $data);

        $response->assertRedirect('/books');
        $response->assertSessionHas('error', 'Book not found');
    }
    
    public function test_Destroyアクション(){
        // モックとフェイクデータ
        $bookId = 1;

        $mockUseCase = \Mockery::mock(DeleteBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId)
            ->andReturnNull();

        $this->app->instance(DeleteBookUseCase::class, $mockUseCase);

        // DELETEリクエスト
        $response = $this->delete("/books/{$bookId}");

        $response->assertRedirect('/books');
        $response->assertSessionHas('success', '本を削除しました');
    }
}
```

## インターフェース アダプタ層のテストコード（API用）

以下のコマンドで インターフェース アダプタ層（API用）の Feature テスト用ファイルの雛形を作成します。

```
$ php artisan make:test ApiBookControllerTest
```

作成された Feature テスト用のファイルを以下のようにしてください。

```php:tests/Feature/ApiBookControllerTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Application\UseCases\GetBookListUseCase;
use App\Application\UseCases\GetBookUseCase;
use App\Application\UseCases\CreateBookUseCase;
use App\Application\UseCases\UpdateBookUseCase;
use App\Application\UseCases\DeleteBookUseCase;
use App\DTOs\BookDto;
use Mockery;

class ApiBookControllerTest extends TestCase
{
    public function test_Indexアクション(): void
    {
        // モックとフェイクデータ
        $bookDtoA = new BookDto(1, 'タイトルA', '著者A', '説明A');
        $bookDtoB = new BookDto(2, 'タイトルB', '著者B', '説明B');
        $books = [$bookDtoA, $bookDtoB];

        $expected = [
            [
                'id' => 1,
                'title' => 'タイトルA',
                'author' => '著者A',
                'description' => '説明A',
            ],
            [
                'id' => 2,
                'title' => 'タイトルB',
                'author' => '著者B',
                'description' => '説明B',
            ],
        ];

        $mockUseCase = Mockery::mock(GetBookListUseCase::class);
        $mockUseCase->shouldReceive('execute')->once()->andReturn($books);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookListUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookListUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get('/api/books');

        $response->assertStatus(200);
        $response->assertJson($expected);
    }

    public function test_Showアクション（成功）(): void
    {
        // モックとフェイクデータ
        $id = 1;
        $bookDto = new BookDto($id, 'タイトルA', '著者A', '説明A');

        $mockUseCase = Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->with($id)
            ->once()
            ->andReturn($bookDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/api/books/{$id}");

        $response->assertStatus(200);
        $response->assertJson([
            'id' => 1,
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ]);
    }

    public function test_Showアクション（該当データなし）(){
        // モックとフェイクデータ
        $id = 999;

        $mockUseCase = Mockery::mock(GetBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->with($id)
            ->once()
            ->andReturn(null);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(GetBookUseCase::class, $mockUseCase);

        // GETリクエスト
        $response = $this->get("/api/books/{$id}");

        $response->assertStatus(404);
        $response->assertJson([
            'message' => '本が見つかりません'
        ]);
    }

    public function test_Storeアクション（成功）(){
        // モックとフェイクデータ
        $title = 'タイトルA';
        $author = '著者A';
        $description = '説明A';

        $mockDto = new BookDto(1, $title, $author, $description);

        // ユースケースのモックを注入
        $mockUseCase = \Mockery::mock(CreateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($title, $author, $description)
            ->andReturn($mockDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する CreateBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(CreateBookUseCase::class, $mockUseCase);

        // POSTリクエスト
        $response = $this->post('/api/books', [
            'title' => $title,
            'author' => $author,
            'description' => $description,
        ]);

        $response->assertStatus(201);
        $response->assertJson([
            'id' => 1,
            'title' => $title,
            'author' => $author,
            'description' => $description,
        ]);
    }

    public function test_Storeアクション（バリデーションエラー：必須チェック）()
    {
        // POSTリクエスト
        $response = $this->post('/api/books', [
            'title' => '',
            'author' => '',
            'description' => '説明A',
        ]);

        $response->assertStatus(422);
        $response->assertJson([
            'errors' => [
                'title' => ['タイトルが入力されていません。'],
                'author' => ['著者が入力されていません。'],
            ]
        ]);
    }

    public function test_Storeアクション（バリデーションエラー：文字数チェック）()
    {
        // POSTリクエスト
        $response = $this->post('/api/books', [
            'title' => 'A',
            'author' => 'A',
            'description' => '説明A',
        ]);

        $response->assertStatus(422);
        $response->assertJson([
            'errors' => [
                'title' => ['タイトルは3文字以上入力してください。'],
                'author' => ['著者は3文字以上入力してください。'],
            ]
        ]);
    }

    public function test_Updateアクション（成功）(){ 
        // モックとフェイクデータ
        $bookId = 1;
        $data = [
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ];
        $bookDto = new BookDto($bookId, $data['title'], $data['author'], $data['description']);

        $mockUseCase = \Mockery::mock(UpdateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId, $data['title'], $data['author'], $data['description'])
            ->andReturn($bookDto);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(UpdateBookUseCase::class, $mockUseCase);

        // PUTリクエスト
        $response = $this->put("/api/books/{$bookId}", $data);

        $response->assertStatus(201);
        $response->assertJson([
            'id' => $bookId,
            'title' => $data['title'],
            'author' => $data['author'],
            'description' => $data['description'],
        ]);
    }

    public function test_Updateアクション（バリデーションエラー：必須チェック）()
    {
        // PUTリクエスト
        $bookId = 1;
        $response = $this->put("/api/books/{$bookId}", [
            'title' => '',
            'author' => '',
            'description' => '説明A',
        ]);

        $response->assertStatus(422);
        $response->assertJson([
            'errors' => [
                'title' => ['タイトルが入力されていません。'],
                'author' => ['著者が入力されていません。'],
            ]
        ]);
    }

    public function test_Updateアクション（バリデーションエラー：文字数チェック）()
    {
        // PUTリクエスト
        $bookId = 1;
        $response = $this->put("/api/books/{$bookId}", [
            'title' => 'A',
            'author' => 'A',
            'description' => '説明A',
        ]);

        $response->assertStatus(422);
        $response->assertJson([
            'errors' => [
                'title' => ['タイトルは3文字以上入力してください。'],
                'author' => ['著者は3文字以上入力してください。'],
            ]
        ]);
    }

    public function test_Updateアクション（該当データなし）(){    
        // モックとフェイクデータ
        $bookId = 999;
        $data = [
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ];

        $mockUseCase = \Mockery::mock(UpdateBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId, $data['title'], $data['author'], $data['description'])
            ->andReturn(null);

        // Laravelのサービスコンテナにモックを登録（コントローラーが依存する GetBookUseCase を実物ではなく「mockUseCase」にすり替える）
        $this->app->instance(UpdateBookUseCase::class, $mockUseCase);

        // PUTリクエスト
        $response = $this->put("/api/books/{$bookId}", $data);

        $response->assertStatus(404);
        $response->assertJson([
            'message' => '本が見つかりません',
        ]);
    }

    public function test_Destroyアクション(){
        // モックとフェイクデータ
        $bookId = 1;

        $mockUseCase = \Mockery::mock(DeleteBookUseCase::class);
        $mockUseCase->shouldReceive('execute')
            ->once()
            ->with($bookId)
            ->andReturnNull();

        $this->app->instance(DeleteBookUseCase::class, $mockUseCase);

        // DELETEリクエスト
        $response = $this->delete("/api/books/{$bookId}");

        $response->assertStatus(204);
    }
}
```

## フレームワーク＆ドライバ層のテストコード

以下のコマンドでフレームワーク＆ドライバ層の Feature テスト用ファイルの雛形を作成します。

```
$ php artisan make:test BookRepositoryTest
```

作成された Feature テスト用のファイルを以下のようにしてください。

```php:tests/Feature/BookRepositoryTest.php
<?php

namespace Tests\Feature\Repositories;

use App\DTOs\BookDto;
use App\Domain\Entities\Book;
use App\Models\Book as EloquentBook;
use App\Infrastructure\Repositories\BookRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookRepositoryTest extends TestCase
{
    use RefreshDatabase;

    private BookRepository $repository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->repository = new BookRepository();
    }

    public function test_DBアクセス：save（新規登録）＆getByIdメソッド(): void
    {
        $dto = new BookDto(null, 'タイトル', '著者', '説明');
        $savedDto = $this->repository->save($dto);

        $this->assertNotNull($savedDto->id);
        $this->assertEquals('タイトル', $savedDto->title);

        $book = $this->repository->getById($savedDto->id);

        $this->assertNotNull($book);
        $this->assertEquals($savedDto->id, $book->getId());
        $this->assertEquals('タイトル', $book->getTitle());
    }

    public function test_DBアクセス：save（更新）メソッド(): void
    {
        $eloquent = EloquentBook::create([
            'title' => 'タイトル（旧）',
            'author' => '著者（旧）',
            'description' => '説明（旧）',
        ]);

        $repository = new BookRepository();

        $bookDto = new BookDto (
            $eloquent->id,
            'タイトル（新）',
            '著者（新）',
            '説明（新）'
        );

        $updatedDto = $repository->save($bookDto);

        $this->assertEquals('タイトル（新）', $updatedDto->title);
        $this->assertDatabaseHas('books', [
            'id' => $eloquent->id,
            'title' => 'タイトル（新）',
        ]);
    }

    public function test_DBアクセス：deleteメソッド()
    {
        $eloquent = EloquentBook::create([
            'title' => 'タイトル',
            'author' => '著者',
            'description' => '説明',
        ]);

        $repository = new BookRepository();
        $repository->delete($eloquent->id);

        $this->assertDatabaseMissing('books', ['id' => $eloquent->id]);
    }

    public function test_DBアクセス：getAllメソッド()
    {
        EloquentBook::create([
            'title' => 'タイトルA',
            'author' => '著者A',
            'description' => '説明A',
        ]);
        EloquentBook::create([
            'title' => 'タイトルB',
            'author' => '著者B',
            'description' => '説明B',
        ]);

        $repository = new BookRepository();
        $books = $repository->getAll();

        $this->assertCount(2, $books);
        $this->assertEquals('タイトルA', $books[0]->title);
        $this->assertEquals('タイトルB', $books[1]->title);
    }
}
```

## 機能（Feature）テストの実行結果

```shell
$ ./vendor/bin/sail artisan test tests/Feature

   PASS  Tests\Feature\ApiBookControllerTest
  ✓ indexアクション                                                                                       0.08s
  ✓ showアクション（成功）                                                                                    0.01s
  ✓ showアクション（該当データなし）                                                                               0.01s
  ✓ storeアクション（成功）                                                                                   0.02s
  ✓ storeアクション（バリデーションエラー：必須チェック）                                                                    0.01s
  ✓ storeアクション（バリデーションエラー：文字数チェック）                                                                   0.01s
  ✓ updateアクション（成功）                                                                                  0.01s
  ✓ updateアクション（バリデーションエラー：必須チェック）                                                                   0.01s
  ✓ updateアクション（バリデーションエラー：文字数チェック）                                                                  0.01s
  ✓ updateアクション（該当データなし）                                                                             0.01s
  ✓ destroyアクション                                                                                     0.01s

   PASS  Tests\Feature\Repositories\BookRepositoryTest
  ✓ d bアクセス：save（新規登録）＆get by idメソッド                                                                 0.88s
  ✓ d bアクセス：save（更新）メソッド                                                                             0.02s
  ✓ d bアクセス：deleteメソッド                                                                               0.01s
  ✓ d bアクセス：get allメソッド                                                                              0.01s

   PASS  Tests\Feature\WebBookControllerTest
  ✓ indexアクション                                                                                       0.02s
  ✓ showアクション（成功）                                                                                    0.01s
  ✓ showアクション（該当データなし）                                                                               0.01s
  ✓ createアクション                                                                                      0.01s
  ✓ storeアクション（成功）                                                                                   0.01s
  ✓ storeアクション（バリデーションエラー：必須チェック）                                                                    0.01s
  ✓ storeアクション（バリデーションエラー：文字数チェック）                                                                   0.01s
  ✓ editアクション（成功）                                                                                    0.01s
  ✓ editアクション（該当データなし）                                                                               0.01s
  ✓ updateアクション（成功）                                                                                  0.01s
  ✓ updateアクション（バリデーションエラー：必須チェック）                                                                   0.01s
  ✓ updateアクション（バリデーションエラー：文字数チェック）                                                                  0.01s
  ✓ updateアクション（該当データなし）                                                                             0.01s
  ✓ destroyアクション                                                                                     0.01s

  Tests:    29 passed (88 assertions)
  Duration: 1.29s
```